⬆️ ⬇️

Security Cheat Sheets: JWT





Many applications use JSON Web Tokens (JWT) to allow the client to identify itself for further information exchange after authentication.



JSON Web Token is an open standard (RFC 7519) that defines a compact and autonomous way to securely transfer information between parties as a JSON object.





This information is verified and reliable because it is digitally signed.

JWTs can be signed using a secret (using the HMAC algorithm) or a public / private key pair using RSA or ECDSA.

')

JSON Web Token is used to transmit information regarding the identity and characteristics of the client. This “container” is signed by the server so that the client does not interfere with it and cannot change, for example, identification data or any characteristics (for example, the role from a simple user to an administrator or change the client’s login).



This token is created in case of successful authentication and is checked by the server before starting the execution of each client request. The token is used by the application as a client’s “identity card” (container with all information about it). The server also has the ability to check the validity and integrity of the token in a secure way. This allows the application to be stateless (the stateless application does not save client data generated in one session for use in the next session with this client (each session is independent)) and the authentication process independent of the services used (in the sense that client and server technologies may vary, including even the transport channel, although HTTP is most often used.



Considerations for using JWT



Even if the JWT token is easy to use and allows you to provide services (mainly REST) ​​stateless (stateless), this solution is not suitable for all applications, because it comes with some reservations, such as the issue of storing the token.



If the application does not have to be completely stateless, then you can consider using the traditional session system provided by all web platforms. However, for stateless applications, JWT is a good option if it is correctly implemented.



JWT issues and attacks



Using the NONE Hash Algorithm



Such an attack occurs when an attacker changes the token and also changes the hashing algorithm (the “alg” field) to indicate via the keyword none that the integrity of the token has already been verified. Some libraries considered tokens signed using the none algorithm as a valid token with a verified signature, so an attacker could change the token payload and the application would trust the token.



To prevent attacks, you must use the JWT library, which is not affected by this vulnerability. Also during the validation check of the token, you must explicitly request the use of the expected algorithm.



Example of implementation:



//  HMAC   String   JVM private transient byte[] keyHMAC = ...; ... //        //    HMAC-256 - JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)).build(); //   DecodedJWT decodedToken = verifier.verify(token); 


Interception Tokens



The attack occurs when the token has been intercepted or stolen by the attacker and he uses it to gain access to the system using the identification data of a specific user.



Protection is to add a “user context” to the token. The user context will consist of the following information:



  1. A random string that is generated at the authentication stage and included in the token, and also sent to the client as a more secure cookie (flags: HttpOnly + Secure + SameSite + cookie prefixes).
  2. The SHA256 hash from a random string will be saved in a token so that any XSS problem will not allow an attacker to read the value of a random string and set the expected cookie.


The IP address will not be used in context, as there are situations in which the IP address may change during a single session, for example, when a user accesses an application through his mobile phone. Then the IP address is constantly legitimately changing. Moreover, the use of an IP address can potentially cause problems at the level of compliance with European GDPR.



If the received token does not contain the correct context during the token check, it must be rejected.

Example of implementation:



Code to create a token after successful authentication:



 //  HMAC   String   JVM private transient byte[] keyHMAC = ...; //    private SecureRandom secureRandom = new SecureRandom(); ... //   ,     byte[] randomFgp = new byte[50]; secureRandom.nextBytes(randomFgp); String userFingerprint = DatatypeConverter.printHexBinary(randomFgp); //    cookie String fingerprintCookie = "__Secure-Fgp=" + userFingerprint + "; SameSite=Strict; HttpOnly; Secure"; response.addHeader("Set-Cookie", fingerprintCookie); // SHA256          // (  )  XSS      //     cookie MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] userFingerprintDigest = digest.digest(userFingerprint.getBytes("utf-8")); String userFingerprintHash = DatatypeConverter.printHexBinary(userFingerprintDigest); //      15     Calendar c = Calendar.getInstance(); Date now = c.getTime(); c.add(Calendar.MINUTE, 15); Date expirationDate = c.getTime(); Map<String, Object> headerClaims = new HashMap<>(); headerClaims.put("typ", "JWT"); String token = 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 verify the validity of the token:

 //  HMAC   String   JVM private transient byte[] keyHMAC = ...; ... //     cookie String userFingerprint = 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(); } } //  SHA256      cookie  //       MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] userFingerprintDigest = digest.digest(userFingerprint.getBytes("utf-8")); String userFingerprintHash = DatatypeConverter.printHexBinary(userFingerprintDigest); //      JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)) .withIssuer(issuerID) .withClaim("userFingerprint", userFingerprintHash) .build(); //   DecodedJWT decodedToken = verifier.verify(token); 


Explicit user token cancellation



Since the token becomes invalid only after its expiration, the user does not have a built-in function that allows the token to be explicitly canceled. Thus, in case of theft, the user cannot withdraw the token by himself and then block the attacker.



One of the methods of protection is the introduction of a black list of tokens, which will be suitable for simulating the function “logout” existing in the traditional session system.



The black list will contain a collection (in SHA-256 encoding in HEX) of a token with a cancellation date that must exceed the validity period of the issued token.



When a user wants to "log out", he calls a special service that adds the provided user token to the blacklist, which leads to the immediate cancellation of the token for further use in the application.



Example of implementation:



Blacklist storage:

For the centralized storage of the blacklist, a database with the following structure will be used:



 create table if not exists revoked_token(jwt_token_digest varchar(255) primary key, revokation_date timestamp default now()); 


Token Cancellation Management:



 //    (logout). //  ,      //         . public class TokenRevoker { //    @Resource("jdbc/storeDS") private DataSource storeDS; //      public boolean isTokenRevoked(String jwtInHex) throws Exception { boolean tokenIsPresent = false; if (jwtInHex != null && !jwtInHex.trim().isEmpty()) { //   byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex); //  SHA256   MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] cipheredTokenDigest = digest.digest(cipheredToken); String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest); //     try (Connection con = this.storeDS.getConnection()) { String query = "select jwt_token_digest from revoked_token where jwt_token_digest = ?"; try (PreparedStatement pStatement = con.prepareStatement(query)) { pStatement.setString(1, jwtTokenDigestInHex); try (ResultSet rSet = pStatement.executeQuery()) { tokenIsPresent = rSet.next(); } } } } return tokenIsPresent; } //    HEX      public void revokeToken(String jwtInHex) throws Exception { if (jwtInHex != null && !jwtInHex.trim().isEmpty()) { //   byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex); //  SHA256   MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] cipheredTokenDigest = digest.digest(cipheredToken); String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest); //             //   if (!this.isTokenRevoked(jwtInHex)) { try (Connection con = this.storeDS.getConnection()) { String query = "insert into revoked_token(jwt_token_digest) values(?)"; int insertedRecordCount; try (PreparedStatement pStatement = con.prepareStatement(query)) { pStatement.setString(1, jwtTokenDigestInHex); insertedRecordCount = pStatement.executeUpdate(); } if (insertedRecordCount != 1) { throw new IllegalStateException("Number of inserted record is invalid," + " 1 expected but is " + insertedRecordCount); } } } } } 


Token Information Disclosure



This attack occurs when an attacker gains access to a token (or a set of tokens) and retrieves the information stored in it (the JWT token information is encoded using base64) to obtain information about the system. Information may be, for example, such as security roles, login format, etc.



The method of protection is quite obvious and is to encrypt the token. It is also important to protect encrypted data from attacks using cryptanalysis. To achieve all of these goals, the AES-GCM algorithm is used, which provides authenticated encryption with associated data (Authenticated Encryption with Associated Data - AEAD). The AEAD primitive provides the functionality of symmetric authenticated encryption. Implementations of this primitive are protected from adaptive attacks based on a selected ciphertext. When encrypting plaintext, you can optionally specify related data that should be authenticated but not encrypted.



That is, encryption with the appropriate data ensures the authenticity and integrity of the data, but not their secrecy.



However, it should be noted that encryption is added mainly to hide internal information, but it is very important to remember that the JWT token’s initial protection against forgery is the signature, so the signature of the token and its verification should always be used.



Client-side storage of tokens



If the application stores the token so that one or more of the following situations occur:





To prevent an attack:



  1. Store the token in a browser using the sessionStorage container.
  2. Add it to the Authorization header using the Bearer scheme. The title should look like this:



     Authorization: Bearer <token> 
  3. Add fingerprint information to token.


By storing the token in the sessionStorage container, it provides the token to be stolen in the case of XSS. However, the fingerprint added to the token prevents the attacker from re-using the stolen token on his computer. To close the maximum areas of use for an attacker, add Content Security Policy to limit the execution context.



The case remains when an attacker uses the user's browsing context as a proxy server to use the target application through a legitimate user, but Content Security Policy can prevent communication with unexpected domains.



It is also possible to implement the authentication service in such a way that the token is issued inside a secure cookie, but in this case protection against CSRF must be implemented.



Using a weak key when creating a token



If the secret used in the case of the HMAC-SHA256 algorithm, which is necessary to sign the token, is weak, then it can be cracked (picked up using a brute force attack). As a result, the attacker can forge an arbitrary valid token from the point of view of the signature.



To prevent this problem, you must use a complex secret key: alphanumeric (mixed case) + special characters.



Since the key is needed only for computer calculations, the size of the secret key can exceed 50 positions.



For example:



 A&'/}Z57M(2hNg=;LE?~]YtRMS5(yZ<vcZTA3N-($>2j:ZeX-BGftaVk`)jKP~q?,jk)EMbgt*kW' 


To assess the complexity of the secret key used to sign your token, you can apply a password dictionary attack to the token in conjunction with the JWT API.

Source: https://habr.com/ru/post/457090/



All Articles