JSON Web Token Cheat Sheet for Java¶
Introduction¶
Many applications useJSON Web Tokens (JWT) to allow the client to indicate its identity for further exchange after authentication.
FromJWT.IO:
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA.
JWTs are used to carry information related to the identity and characteristics (claims) of a client. This information is signed by the server to ensure it has not been tampered with after being sent to the client. This prevents an attacker from modifying the identity or characteristics — for example, changing the role from a simple user to an admin or altering the client's login.
The token is created during authentication (it is issued upon successful authentication) and is verified by the server before any processing. Applications use the token to allow a client to present what is essentially an "identity card" to the server. The server can then securely verify the token's validity and integrity. This approach is stateless and portable, meaning it works across different client and server technologies, and over various transport channels — although HTTP is the most commonly used.
Token Structure¶
Token structure example taken fromJWT.IO:
[Base64(HEADER)].[Base64(PAYLOAD)].[Base64(SIGNATURE)]
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQChunk 1:Header
{"alg":"HS256","typ":"JWT"}Chunk 2:Payload
{"sub":"1234567890","name":"John Doe","admin":true}Chunk 3:Signature
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),KEY)Objective¶
This cheatsheet provides tips to prevent common security issues when using JSON Web Tokens (JWT) with Java.
The tips presented in this article are part of a Java project that was created to show the correct way to handle creation and validation of JSON Web Tokens.
You can find the Java projecthere, it uses the officialJWT library.
In the rest of the article, the termtoken refers to theJSON Web Tokens (JWT).
Consideration about Using JWT¶
Even if a JWT is "easy" to use and allow to expose services (mostly REST style) in a stateless way, it's not the solution that fits for all applications because it comes with some caveats, like for example the question of the storage of the token (tackled in this cheatsheet) and others...
If your application does not need to be fully stateless, you can consider using traditional session system provided by all web frameworks and follow the advice from the dedicatedsession management cheat sheet. However, for stateless applications, when well implemented, it's a good candidate.
Issues¶
None Hashing Algorithm¶
Symptom¶
This attack, describedhere, occurs when an attacker alters the token and changes the hashing algorithm to indicate, through thenone keyword, that the integrity of the token has already been verified. As explained in the link abovesome libraries treated tokens signed with the none algorithm as a valid token with a verified signature, so an attacker can alter the token claims and the modified token will still be trusted by the application.
How to Prevent¶
First, use a JWT library that is not exposed to this vulnerability.
Last, during token validation, explicitly request that the expected algorithm was used.
Implementation Example¶
// HMAC key - Block serialization and storage as String in JVM memoryprivatetransientbyte[]keyHMAC=...;...//Create a verification context for the token requesting//explicitly the use of the HMAC-256 hashing algorithmJWTVerifierverifier=JWT.require(Algorithm.HMAC256(keyHMAC)).build();//Verify the token, if the verification fail then a exception is thrownDecodedJWTdecodedToken=verifier.verify(token);Token Sidejacking¶
Symptom¶
This attack occurs when a token has been intercepted/stolen by an attacker and they use it to gain access to the system using targeted user identity.
How to Prevent¶
One way to prevent this is by adding a "user context" to the token. The user context should consist of the following:
- A random string generated during the authentication phase. This string is sent to the client as a hardened cookie (with the following flags:HttpOnly + Secure,SameSite,Max-Age, andcookie prefixes). Avoid setting theexpires header so the cookie is cleared when the browser is closed. SetMax-Age to a value equal to or less than the JWT's expiry time — never more.
- A SHA256 hash of the random string will be stored in the token (instead of the raw value) in order to prevent any XSS issues allowing the attacker to read the random string value and setting the expected cookie.
Avoid using IP addresses as part of the context. IP addresses can change during a single session due to legitimate reasons — for example, when a user accesses the application on a mobile device and switches network providers. Additionally, IP tracking can raise concerns related toGDPR compliance in the EU.
During token validation, if the received token does not contain the correct context (e.g., if it is being replayed by an attacker), it must be rejected.
Implementation example¶
Code to create the token after successful authentication.
// HMAC key - Block serialization and storage as String in JVM memoryprivatetransientbyte[]keyHMAC=...;// Random data generatorprivateSecureRandomsecureRandom=newSecureRandom();...//Generate a random string that will constitute the fingerprint for this userbyte[]randomFgp=newbyte[50];secureRandom.nextBytes(randomFgp);StringuserFingerprint=DatatypeConverter.printHexBinary(randomFgp);//Add the fingerprint in a hardened cookie - Add cookie manually because//SameSite attribute is not supported by javax.servlet.http.Cookie classStringfingerprintCookie="__Secure-Fgp="+userFingerprint+"; SameSite=Strict; HttpOnly; Secure";response.addHeader("Set-Cookie",fingerprintCookie);//Compute a SHA256 hash of the fingerprint in order to store the//fingerprint hash (instead of the raw value) in the token//to prevent an XSS to be able to read the fingerprint and//set the expected cookie itselfMessageDigestdigest=MessageDigest.getInstance("SHA-256");byte[]userFingerprintDigest=digest.digest(userFingerprint.getBytes("utf-8"));StringuserFingerprintHash=DatatypeConverter.printHexBinary(userFingerprintDigest);//Create the token with a validity of 15 minutes and client context (fingerprint) informationCalendarc=Calendar.getInstance();Datenow=c.getTime();c.add(Calendar.MINUTE,15);DateexpirationDate=c.getTime();Map<String,Object>headerClaims=newHashMap<>();headerClaims.put("typ","JWT");Stringtoken=JWT.create().withSubject(login).withExpiresAt(expirationDate).withIssuer(this.issuerID).withIssuedAt(now).withNotBefore(now).withClaim("userFingerprint",userFingerprintHash).withHeader(headerClaims).sign(Algorithm.HMAC256(this.keyHMAC));Code to validate the token.
// HMAC key - Block serialization and storage as String in JVM memoryprivatetransientbyte[]keyHMAC=...;...//Retrieve the user fingerprint from the dedicated cookieStringuserFingerprint=null;if(request.getCookies()!=null&&request.getCookies().length>0){List<Cookie>cookies=Arrays.stream(request.getCookies()).collect(Collectors.toList());Optional<Cookie>cookie=cookies.stream().filter(c->"__Secure-Fgp".equals(c.getName())).findFirst();if(cookie.isPresent()){userFingerprint=cookie.get().getValue();}}//Compute a SHA256 hash of the received fingerprint in cookie in order to compare//it to the fingerprint hash stored in the tokenMessageDigestdigest=MessageDigest.getInstance("SHA-256");byte[]userFingerprintDigest=digest.digest(userFingerprint.getBytes("utf-8"));StringuserFingerprintHash=DatatypeConverter.printHexBinary(userFingerprintDigest);//Create a verification context for the tokenJWTVerifierverifier=JWT.require(Algorithm.HMAC256(keyHMAC)).withIssuer(issuerID).withClaim("userFingerprint",userFingerprintHash).build();//Verify the token, if the verification fail then an exception is thrownDecodedJWTdecodedToken=verifier.verify(token);No Built-In Token Revocation by the User¶
Symptom¶
This problem is inherent to JWT because a token only becomes invalid when it expires. The user has no built-in feature to explicitly revoke the validity of a token. This means that if it is stolen, a user cannot revoke the token itself thereby blocking the attacker.
How to Prevent¶
Since JWTs are stateless, There is no session maintained on the server(s) serving client requests. As such, there is no session to invalidate on the server side. A well implemented Token Sidejacking solution (as explained above) should alleviate the need for maintaining denylist on server side. This is because a hardened cookie used in the Token Sidejacking can be considered as secure as a session ID used in the traditional session system, and unless both the cookie and the JWT are intercepted/stolen, the JWT is unusable. A logout can thus be 'simulated' by clearing the JWT from session storage. If the user chooses to close the browser instead, then both the cookie and sessionStorage are cleared automatically.
Another way to protect against this is to implement a token denylist that will be used to mimic the "logout" feature that exists with traditional session management system.
The denylist will keep a digest (SHA-256 encoded in HEX) of the token with a revocation date. This entry must endure at least until the expiration of the token.
When the user wants to "logout" then it call a dedicated service that will add the provided user token to the denylist resulting in an immediate invalidation of the token for further usage in the application.
Implementation Example¶
Block List Storage¶
A database table with the following structure will be used as the central denylist storage.
createtableifnotexistsrevoked_token(jwt_token_digestvarchar(255)primarykey,revocation_datetimestampdefaultnow());Token Revocation Management¶
Code in charge of adding a token to the denylist and checking if a token is revoked.
/*** Handle the revocation of the token (logout).* Use a DB in order to allow multiple instances to check for revoked token* and allow cleanup at centralized DB level.*/publicclassTokenRevoker{/** DB Connection */@Resource("jdbc/storeDS")privateDataSourcestoreDS;/** * Verify if a digest encoded in HEX of the ciphered token is present * in the revocation table * * @param jwtInHex Token encoded in HEX * @return Presence flag * @throws Exception If any issue occur during communication with DB */publicbooleanisTokenRevoked(StringjwtInHex)throwsException{booleantokenIsPresent=false;if(jwtInHex!=null&&!jwtInHex.trim().isEmpty()){//Decode the ciphered tokenbyte[]cipheredToken=DatatypeConverter.parseHexBinary(jwtInHex);//Compute a SHA256 of the ciphered tokenMessageDigestdigest=MessageDigest.getInstance("SHA-256");byte[]cipheredTokenDigest=digest.digest(cipheredToken);StringjwtTokenDigestInHex=DatatypeConverter.printHexBinary(cipheredTokenDigest);//Search token digest in HEX in DBtry(Connectioncon=this.storeDS.getConnection()){Stringquery="select jwt_token_digest from revoked_token where jwt_token_digest = ?";try(PreparedStatementpStatement=con.prepareStatement(query)){pStatement.setString(1,jwtTokenDigestInHex);try(ResultSetrSet=pStatement.executeQuery()){tokenIsPresent=rSet.next();}}}}returntokenIsPresent;}/** * Add a digest encoded in HEX of the ciphered token to the revocation token table * * @param jwtInHex Token encoded in HEX * @throws Exception If any issue occur during communication with DB */publicvoidrevokeToken(StringjwtInHex)throwsException{if(jwtInHex!=null&&!jwtInHex.trim().isEmpty()){//Decode the ciphered tokenbyte[]cipheredToken=DatatypeConverter.parseHexBinary(jwtInHex);//Compute a SHA256 of the ciphered tokenMessageDigestdigest=MessageDigest.getInstance("SHA-256");byte[]cipheredTokenDigest=digest.digest(cipheredToken);StringjwtTokenDigestInHex=DatatypeConverter.printHexBinary(cipheredTokenDigest);//Check if the token digest in HEX is already in the DB and add it if it is absentif(!this.isTokenRevoked(jwtInHex)){try(Connectioncon=this.storeDS.getConnection()){Stringquery="insert into revoked_token(jwt_token_digest) values(?)";intinsertedRecordCount;try(PreparedStatementpStatement=con.prepareStatement(query)){pStatement.setString(1,jwtTokenDigestInHex);insertedRecordCount=pStatement.executeUpdate();}if(insertedRecordCount!=1){thrownewIllegalStateException("Number of inserted record is invalid,"+" 1 expected but is "+insertedRecordCount);}}}}}Token Information Disclosure¶
Symptom¶
This attack occurs when an attacker has access to a token (or a set of tokens) and extracts information stored in it (the contents of JWTs are base64 encoded, but is not encrypted by default) in order to obtain information about the system. Information can be for example the security roles, login format...
How to Prevent¶
A way to protect against this attack is to cipher the token using, for example, a symmetric algorithm.
It's also important to protect the ciphered data against attack likePadding Oracle or any other attack using cryptanalysis.
In order to achieve all these goals, theAES-GCM algorithm is used which providesAuthenticated Encryption with Associated Data.
More details fromhere:
AEAD primitive (Authenticated Encryption with Associated Data) provides functionality of symmetricauthenticated encryption.Implementations of this primitive are secure against adaptive chosen ciphertext attacks.When encrypting a plaintext one can optionally provide associated data that should be authenticatedbut not encrypted.That is, the encryption with associated data ensures authenticity (ie. who the sender is) andintegrity (ie. data has not been tampered with) of that data, but not its secrecy.See RFC5116: https://tools.ietf.org/html/rfc5116Note:
Here ciphering is added mainly to hide internal information but it's very important to remember that the first protection against tampering of the JWT is the signature. So, the token signature and its verification must be always in place.
Implementation Example¶
Token Ciphering¶
Code in charge of managing the ciphering.Google Tink dedicated crypto library is used to handle ciphering operations in order to use built-in best practices provided by this library.
/** * Handle ciphering and deciphering of the token using AES-GCM. * * @see "https://github.com/google/tink/blob/master/docs/JAVA-HOWTO.md" */publicclassTokenCipher{/** * Constructor - Register AEAD configuration * * @throws Exception If any issue occur during AEAD configuration registration */publicTokenCipher()throwsException{AeadConfig.register();}/** * Cipher a JWT * * @param jwt Token to cipher * @param keysetHandle Pointer to the keyset handle * @return The ciphered version of the token encoded in HEX * @throws Exception If any issue occur during token ciphering operation */publicStringcipherToken(Stringjwt,KeysetHandlekeysetHandle)throwsException{//Verify parametersif(jwt==null||jwt.isEmpty()||keysetHandle==null){thrownewIllegalArgumentException("Both parameters must be specified!");}//Get the primitiveAeadaead=AeadFactory.getPrimitive(keysetHandle);//Cipher the tokenbyte[]cipheredToken=aead.encrypt(jwt.getBytes(),null);returnDatatypeConverter.printHexBinary(cipheredToken);}/** * Decipher a JWT * * @param jwtInHex Token to decipher encoded in HEX * @param keysetHandle Pointer to the keyset handle * @return The token in clear text * @throws Exception If any issue occur during token deciphering operation */publicStringdecipherToken(StringjwtInHex,KeysetHandlekeysetHandle)throwsException{//Verify parametersif(jwtInHex==null||jwtInHex.isEmpty()||keysetHandle==null){thrownewIllegalArgumentException("Both parameters must be specified !");}//Decode the ciphered tokenbyte[]cipheredToken=DatatypeConverter.parseHexBinary(jwtInHex);//Get the primitiveAeadaead=AeadFactory.getPrimitive(keysetHandle);//Decipher the tokenbyte[]decipheredToken=aead.decrypt(cipheredToken,null);returnnewString(decipheredToken);}}Creation / Validation of the Token¶
Use the token ciphering handler during the creation and the validation of the token.
Load keys (ciphering key was generated and stored usingGoogle Tink) and setup cipher.
//Load keys from configuration text/json files in order to avoid to storing keys as a String in JVM memoryprivatetransientbyte[]keyHMAC=Files.readAllBytes(Paths.get("src","main","conf","key-hmac.txt"));privatetransientKeysetHandlekeyCiphering=CleartextKeysetHandle.read(JsonKeysetReader.withFile(Paths.get("src","main","conf","key-ciphering.json").toFile()));...//Init token ciphering handlerTokenCiphertokenCipher=newTokenCipher();Token creation.
//Generate the JWT token using the JWT API...//Cipher the token (String JSON representation)StringcipheredToken=tokenCipher.cipherToken(token,this.keyCiphering);//Send the ciphered token encoded in HEX to the client in HTTP response...Token validation.
//Retrieve the ciphered token encoded in HEX from the HTTP request...//Decipher the tokenStringtoken=tokenCipher.decipherToken(cipheredToken,this.keyCiphering);//Verify the token using the JWT API...//Verify access...Token Storage on Client Side¶
Symptom¶
This occurs when an application stores the token in a manner exhibiting the following behavior:
- Automatically sent by the browser (Cookie storage).
- Retrieved even if the browser is restarted (Use of browserlocalStorage container).
- Retrieved in case ofXSS issue (Cookie accessible to JavaScript code or Token stored in browser local/session storage).
How to Prevent¶
- Store the token using the browsersessionStorage container, or use JavaScriptclosures withprivate variables
- Add it as aBearer HTTP
Authenticationheader with JavaScript when calling services. - Addfingerprint information to the token.
By storing the token in browsersessionStorage container it exposes the token to being stolen through an XSS attack. However, fingerprints added to the token prevent reuse of the stolen token by the attacker on their machine. To close a maximum of exploitation surfaces for an attacker, add a browserContent Security Policy to harden the execution context.
But, we know thatsessionStorage is not always practical due to its per-tab scope, and the storage method for tokens should balancesecurity andusability.
LocalStorage is a better method thansessionStorage for usability because it allows the session to persist between browser restarts and across tabs, but you must use strict security controls:
- Tokens stored inlocalStorage should haveshort expiration times (e.g.,15-30 minutes idle timeout, 8-hour absolute timeout).
- Implement mechanisms such astoken rotation andrefresh tokens to minimize risk.
Ifsession persistence across tabs andsessionStorage are required, consider usingBroadcastChannel API orSingle Sign-On (SSO) to re-authenticate users automatically when they open new tabs.
An alternative to storing token in browsersessionStorage or inlocalStorage is to use JavaScript private variable or Closures. In this, access to all web requests are routed through a JavaScript module that encapsulates the token in a private variable which can not be accessed other than from within the module.
Note:
- The remaining case is when an attacker uses the user's browsing context as a proxy to use the target application through the legitimate user but the Content Security Policy can prevent communication with non expected domains.
- It's also possible to implement the authentication service in a way that the token is issued within a hardened cookie, but in this case, protection against aCross-Site Request Forgery attack must be implemented.
Implementation Example¶
JavaScript code to store the token after authentication.
/* Handle request for JWT token and local storage*/functionauthenticate(){constlogin=$("#login").val();constpostData="login="+encodeURIComponent(login)+"&password=test";$.post("/services/authenticate",postData,function(data){if(data.status=="Authentication successful!"){...sessionStorage.setItem("token",data.token);}else{...sessionStorage.removeItem("token");}}).fail(function(jqXHR,textStatus,error){...sessionStorage.removeItem("token");});}JavaScript code to add the token as aBearer HTTP Authentication header when calling a service, for example a service to validate token here.
/* Handle request for JWT token validation */functionvalidateToken(){vartoken=sessionStorage.getItem("token");if(token==undefined||token==""){$("#infoZone").removeClass();$("#infoZone").addClass("alert alert-warning");$("#infoZone").text("Obtain a JWT token first :)");return;}$.ajax({url:"/services/validate",type:"POST",beforeSend:function(xhr){xhr.setRequestHeader("Authorization","bearer "+token);},success:function(data){...},error:function(jqXHR,textStatus,error){...},});}JavaScript code to implement closures with private variables:
functionmyFetchModule(){// Protect the original 'fetch' from getting overwritten via XSSconstfetch=window.fetch;constauthOrigins=["https://yourorigin","http://localhost"];lettoken='';this.setToken=(value)=>{token=value}this.fetch=(resource,options)=>{letreq=newRequest(resource,options);destOrigin=newURL(req.url).origin;if(token&&authOrigins.includes(destOrigin)){req.headers.set('Authorization',token);}returnfetch(req)}}...// usage:constmyFetch=newmyFetchModule()functionlogin(){fetch("/api/login").then((res)=>{if(res.status==200){returnres.json()}else{throwError(res.statusText)}}).then(data=>{myFetch.setToken(data.token)console.log("Token received and stored.")}).catch(console.error)}...// after login, subsequent api calls:functionmakeRequest(){myFetch.fetch("/api/hello",{headers:{"MyHeader":"foobar"}}).then((res)=>{if(res.status==200){returnres.text()}else{throwError(res.statusText)}}).then(responseText=>console.log("helloResponse",responseText)).catch(console.error)}Weak Token Secret¶
Symptom¶
When the token is protected using an HMAC based algorithm, the security of the token is entirely dependent on the strength of the secret used with the HMAC. If an attacker can obtain a valid JWT, they can then carry out an offline attack and attempt to crack the secret using tools such asJohn the Ripper orHashcat.
If they are successful, they would then be able to modify the token and re-sign it with the key they had obtained. This could let them escalate their privileges, compromise other users' accounts, or perform other actions depending on the contents of the JWT.
There are a number ofguides that document this process in greater detail.
How to Prevent¶
The simplest way to prevent this attack is to ensure that the secret used to sign the JWTs is strong and unique, in order to make it harder for an attacker to crack. As this secret would never need to be typed by a human, it should be at least 64 characters, and generated using asecure source of randomness.
Alternatively, consider the use of tokens that are signed with RSA rather than using an HMAC and secret key.
Further Reading¶
- {JWT}.{Attack}.Playbook - A project documents the known attacks and potential security vulnerabilities and misconfigurations of JSON Web Tokens.
- JWT Best Practices Internet Draft