This site is the archived OWASP Foundation Wiki and is no longer accepting Account Requests.
To view the new OWASP Foundation website, please visit https://owasp.org

JSON Web Token (JWT) Cheat Sheet for Java

From OWASP
Revision as of 06:07, 11 September 2017 by Jmanico (talk | contribs)

Jump to: navigation, search
Cheatsheets-header.jpg

Last revision (mm/dd/yy): 09/11/2017

Introduction

Many application use JSON Web Tokens (JWT) to allow the client to indicate is identity for further exchange after authentication.

From https://jwt.io/introduction:

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.

JSON Web Token is used to carry information related to the identity and characteristics (claims) of a client. This "container" is signed by the server in order to avoid that a client tamper it in order to change, for example, the identity or any characteristics (example: change the role from simple user to admin or change the client login). This token is created during authentication (is provided in case of successful authentication) and is verified by the server before any processing. It is used by an application to allow a client to present a token representing his "identity card" (container with all user information about him) to server and allow the server to verify the validity and integrity of the token in a secure way, all of this in a stateless and portable approach (portable in the way that client and server technologies can be different including also the transport channel even if HTTP is the most often used).

Token structure

Token structure example taken from https://jwt.io/#debugger:

[ Base64(HEADER) ] . [ Base64(PAYLOAD) ] . [ Base64(SIGNATURE) ]

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Chunk 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 article have for objective to provide several tips in a form of a cheats sheet, for Java technology, in order to prevent common security issues meet when using JWT.

About the code samples provided in tips, a global java project sample has been created in order to show the creation and the validation of a token in a secure way.

This project is available here and it use official JWT library with Crypto-JS library.

In the rest of the article, the term token refer to the JSON Web Tokens (JWT).

Issues

NONE hashing algorithm

Symptom

This attack, described here occur when a attacker alter the token and change the hashing algorithm to indicate, through, the none keyword, that the integrity of the token has already been verified. As explained in the link above some 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 tkey will 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 memory
private transient byte[] keyHMAC = ...;

...

//Create a verification context for the token requesting explicitly the use of the HMAC-256 hashing algorithm
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)).build();

//Verify the token, if the verification fail then a exception is throwed
DecodedJWT decodedToken = verifier.verify(token);

Token sidejacking

Symptom

This attack occur when a token has been intercepted/stolen by a attacker and this one use it to gain access to the system using targeted user identity.

How to prevent

A way to protect, is to add "user context" in the token. User context here can be composed by the following information:

  • Browser unique fingerprint.
  • IP address: Use this context information when the target users are expected to use the site/service for a "single objective" and during a "short" amount of time like for example a wire transfer in a bank (connect, perform the transfer and disconnect). There some situation in which IP address can change during the same session like for example when a user access an application through his mobile and he change mobile operator then he change legitimately (often) is IP address. In the same way don’t use only the IP address because this context information will not uniquely identify a user if he access the application through a proxy (for example a corporate proxy).

During token validation, if the received token do not contains the rights context so, it is replayed and then it must be rejected.

Implementation example

Client side: Generate a browser unique fingerprint

JavaScript code to generate the browser fingerprint.

/* Generate a fingerprint string for the browser */
function generateFingerprint(){
    //Generate a string based on "stable" information taken from the browser
    //We call here "stable information", information that normally don't change during the user
    //browse the application just after authentication
    var fingerprint = [];

    //Take plugins
    for(var i = 0; i < navigator.plugins.length; i++){
       fingerprint.push(navigator.plugins[i].name);
       fingerprint.push(navigator.plugins[i].filename);
       fingerprint.push(navigator.plugins[i].description);
       fingerprint.push(navigator.plugins[i].version);
    }

    //Take User Agent
    fingerprint.push(navigator.userAgent);

    //Take Screen resolution
    fingerprint.push(screen.availHeight);
    fingerprint.push(screen.availWidth);
    fingerprint.push(screen.colorDepth);
    fingerprint.push(screen.height);
    fingerprint.push(screen.pixelDepth);
    fingerprint.push(screen.width);

    //Take Graphical card info
    //See http://output.jsbin.com/ovekor/3/
    try {
        //Add a Canvas element if the body do not contains one
        if ( $("#glcanvas").length == 0 ){
            $(document.body).append("<canvas id='glcanvas'></canvas>");
        }
        //Get ref on Canvas
        var canvas = document.getElementById("glcanvas");
        //Retrieve Canvas properties
	    gl = canvas.getContext("experimental-webgl");
	    gl.viewportWidth = canvas.width;
	    gl.viewportHeight = canvas.height;
        fingerprint.push(gl.getParameter(gl.VERSION));
        fingerprint.push(gl.getParameter(gl.SHADING_LANGUAGE_VERSION));
        fingerprint.push(gl.getParameter(gl.VENDOR));
        fingerprint.push(gl.getParameter(gl.RENDERER));
        fingerprint.push(gl.getSupportedExtensions().join());
    } catch (e) {
        //Get also error because it's will be stable too..
        fingerprint.push(e);
    }

    //Last and, in order to made this browser unique, generate a random ID that we will store
    //in local storage (in order to be persistent after browser close/reopen)
    //Add this ID because, in Enterprise, most of the time browser have the same configuration
    var browserUniqueID = localStorage.getItem("browserUniqueID");
    if (browserUniqueID === null) {
      localStorage.setItem("browserUniqueID", CryptoJS.lib.WordArray.random(80));
      browserUniqueID = localStorage.getItem("browserUniqueID");
    }
    fingerprint.push(browserUniqueID);

    return fingerprint.join();
}


JavaScript code to generate a hash of the fingerprint that will be sent to the server for each request. A hash is send in order to avoid to send user browser properties on the wire.

//Call the fingerprint dedicated function
var fingerprint = generateFingerprint();
//Use CryptoJS library ot generate a hex encoded string of the hash of the fingerprint
var fingerprintHash = CryptoJS.SHA256(fingerprint);


Server side: Token creation and validation

Code to extract the client IP.

Remark:

An issue exists in this implementation proposal about the retrieving of the client IP. Indeed, if the header X-Forwarded-For is present then the method retrieveClientIP() will get the client IP from this header according to the header expected content.

The issue occur when the Web Application Firewall append the existing header X-Forwarded-For of the incoming request (add the client real network IP at the end of the list) instead of overwriting the header content with the real network IP of the incoming request. In this way, an attacker can add the header X-Forwarded-For in his request and set an arbitrary IP that will be taken as the client IP by the the method retrieveClientIP().

If you don't have any Web Application Firewall or Reverse Proxy in front of your application then use directly the IP from the HttpServletRequest instance.

/**
* Return the client IP address
* @param request Incoming HTTP request
* @return The client IP address
* @throws IllegalArgumentException If the IP retrieved is invalid
* @see "https://en.wikipedia.org/wiki/X-Forwarded-For"
*/
private String retrieveClientIP(HttpServletRequest request) throws IllegalArgumentException{
	String address;
	//Get IP from X-Forwarded-For header or from the request object directly
	String ip = request.getHeader("X-Forwarded-For");
	if(ip != null){
	    if(ip.contains(",")){
		ip = ip.split(",")[0];
	    }
	    address = ip.trim();
	}else{
	    address = request.getRemoteAddr();
	}
	//Validate IP format
	//"InetAddressValidator" came from "commons-validator" API
	if(!InetAddressValidator.getInstance().isValid(address)){
	    throw new IllegalArgumentException("Invalid IP address !");
	}

	return address;
}


Code to create the token after success authentication.

// HMAC key - Block serialization and storage as String in JVM memory
private transient byte[] keyHMAC = ...;

...

//Create the token with a validity of 15 minutes and client context (IP + Browser fingerprint SHA256 digest HEX encoded) information
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(issuerID)
                       .withIssuedAt(now)
                       .withNotBefore(now)
                       .withClaim("clientIP", retrieveClientIP(request))
                       .withClaim("browserFingerprintDigest", browserFingerprintDigest)
                       .withHeader(headerClaims)
                       .sign(Algorithm.HMAC256(keyHMAC));


Code to validate the token.

// HMAC key - Block serialization and storage as String in JVM memory
private transient byte[] keyHMAC = ...;

...

//Create a verification context for the token
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC))
                               .withIssuer(issuerID)
                               .withClaim("clientIP", retrieveClientIP(request))
                               .withClaim("browserFingerprintDigest", browserFingerprintDigest)
                               .build();

//Verify the token, if the verification fail then a exception is throwed
DecodedJWT decodedToken = verifier.verify(token);


Token information disclosure

Symptom

This attack occur when a attacker access to a token (or a set of tokens) and extract information stored into it (JWT token information are base64 encoded at the basis) in order to obtains information about the system. Information can be for example the security roles, login format...

How to prevent

A way to protect, is to cipher the token using for example a symetric algorithm.

It's also important to protect the ciphered data against attack like Padding Oracle or any other attack using cryptanalysis.

In order to achieve all these goals, the algorithm AES-GCM can be used in conjunction with Additional Authentication Data (AAD) feature.

A database can be used to store the NONCE and the AAD associated to a token.

Note:

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 token is the signature so, the token signature and is verification must be always in place.

Implementation example

Token ciphering

Database structure.

create table if not exists nonce(jwt_token_digest varchar(255) primary key, gcm_nonce varchar(255) not null unique, gcm_aad varchar(255) not null unique);
create index if not exists idx_nonce on nonce(gcm_nonce);


Code in charge of managing the ciphering.

/**
 * Handle ciphering and deciphering of the token using AES-GCM.
 * Use a DB in order to link a GCM NONCE to a ciphered message and ensure that a NONCE is never reused
 * and also allow use of several application nodes in load balancing.
 */
public class TokenCipher {

    /** AES-GCM parameters */
    private static final int GCM_NONCE_LENGTH = 12; // in bytes

    /** AES-GCM parameters */
    private static final int GCM_TAG_LENGTH = 16; // in bytes

    /**Secure random generator */
    private final SecureRandom secRandom = new SecureRandom();

    /** DB Connection */
    @Resource("jdbc/storeDS")
    private DataSource storeDS;

    /**
     * Cipher a JWT
     * @param jwt Token to cipher
     * @param key Ciphering key
     * @return The ciphered version of the token encoded in HEX
     * @throws Exception If any issue occur during token ciphering operation
     */
    public String cipherToken(String jwt, byte[] key) throws Exception {
        //Verify parameters
        if(jwt == null || jwt.isEmpty() || key == null || key.length == 0){
            throw new IllegalArgumentException("Both parameters must be specified !");
        }

        //Generate a NONCE
        //NOTE: As in the DB, the column to store the NONCE is flagged UNIQUE then the insert will fail
        //if the NONCE already exists, normally as we use the Java Secure Random implementation
        //it will never happen.
        final byte[] nonce = new byte[GCM_NONCE_LENGTH];
        secRandom.nextBytes(nonce);

        //Prepare ciphering key from bytes provided
        SecretKey aesKey = new SecretKeySpec(key, 0, key.length, "AES");

        //Setup Cipher
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce);
        cipher.init(Cipher.ENCRYPT_MODE, aesKey, spec);

        //Add "Additional Authentication Data" (AAD) in order to operate in AEAD mode - Generate it
        byte[] aad = new byte[32];
        secRandom.nextBytes(aad);
        cipher.updateAAD(aad);

        //Cipher the token
        byte[] cipheredToken = cipher.doFinal(jwt.getBytes("utf-8"));

        //Compute a SHA256 of the ciphered token
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] cipheredTokenDigest = digest.digest(cipheredToken);

        //Store GCM NONCE and GCM AAD
        this.storeNonceAndAAD(DatatypeConverter.printHexBinary(nonce), DatatypeConverter.printHexBinary(aad),
        DatatypeConverter.printHexBinary(cipheredTokenDigest));

        return DatatypeConverter.printHexBinary(cipheredToken);
    }

    /**
     * Decipher a JWT
     * @param jwtInHex Token to decipher encoded in HEX
     * @param key Ciphering key
     * @return The token in clear text
     * @throws Exception If any issue occur during token deciphering operation
     */
    public String decipherToken(String jwtInHex, byte[] key) throws Exception{
        //Verify parameters
        if(jwtInHex == null || jwtInHex.isEmpty() || key == null || key.length == 0){
            throw new IllegalArgumentException("Both parameters must be specified !");
        }

        //Decode the ciphered token
        byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex);

        //Compute a SHA256 of the ciphered token
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] cipheredTokenDigest = digest.digest(cipheredToken);

        //Read the GCM NONCE and GCM AAD associated from the DB
        Map<String,String> gcmInfos = this.readNonceAndAAD(DatatypeConverter.printHexBinary(cipheredTokenDigest));
        if(gcmInfos == null){
            throw new Exception("Cannot found a NONCE and AAD associated to the token provided !");
        }
        byte[] nonce = DatatypeConverter.parseHexBinary(gcmInfos.get("NONCE"));
        byte[] aad = DatatypeConverter.parseHexBinary(gcmInfos.get("AAD"));

        //Prepare ciphering key from bytes provided
        SecretKey aesKey = new SecretKeySpec(key, 0, key.length, "AES");

        //Setup Cipher
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce);
        cipher.init(Cipher.DECRYPT_MODE, aesKey, spec);

        //Add "Additional Authentication Data" (AAD) in order to operate in AEAD mode
        cipher.updateAAD(aad);

        //Decipher the token
        byte[] decipheredToken = cipher.doFinal(cipheredToken);

        return new String(decipheredToken);
    }


    /**
     * Store GCM NONCE and GCM AAD in the DB
     * @param nonceInHex Nonce encoded in HEX
     * @param aadInHex AAD encoded in HEX
     * @param jwtTokenDigestInHex SHA256 of the JWT ciphered token encoded in HEX
     * @throws Exception If any issue occur during communication with DB
     */
    private void storeNonceAndAAD(String nonceInHex, String aadInHex, String jwtTokenDigestInHex) throws Exception {
        try (Connection con = this.storeDS.getConnection()) {
            String query = "insert into nonce(jwt_token_digest, gcm_nonce, gcm_aad) values(?, ?, ?)";
            int insertedRecordCount;
            try (PreparedStatement pStatement = con.prepareStatement(query)) {
                pStatement.setString(1, jwtTokenDigestInHex);
                pStatement.setString(2, nonceInHex);
                pStatement.setString(3, aadInHex);
                insertedRecordCount = pStatement.executeUpdate();
            }
            if (insertedRecordCount != 1) {
                throw new IllegalStateException("Number of inserted record is invalid, 1 expected but is " + insertedRecordCount);
            }
        }
    }

    /**
     * Read GCM NONCE and GCM AAD from the DB
     * @param jwtTokenDigestInHex SHA256 of the JWT ciphered token encoded in HEX for which we must read the NONCE and AAD
     * @return A dict containing the NONCE and AAD if they exists for the specified token
     * @throws Exception If any issue occur during communication with DB
     */
    private  Map<String,String> readNonceAndAAD(String jwtTokenDigestInHex) throws Exception{
        Map<String,String> gcmInfos = null;
        try (Connection con = this.storeDS.getConnection()) {
            String query = "select gcm_nonce, gcm_aad from nonce where jwt_token_digest = ?";
            try (PreparedStatement pStatement = con.prepareStatement(query)) {
                pStatement.setString(1, jwtTokenDigestInHex);
                try (ResultSet rSet = pStatement.executeQuery()) {
                    while (rSet.next()) {
                        gcmInfos = new HashMap<>(2);
                        gcmInfos.put("NONCE", rSet.getString(1));
                        gcmInfos.put("AAD", rSet.getString(2));
                    }
                }
            }
        }

        return gcmInfos;
    }

}

Creation / Validation of the token

Use of the token ciphering during the creation and the validation of the token.

Load keys and setup cipher.

//Load keys from configuration text files in order to avoid to store keys as String in JVM memory
private transient byte[] keyHMAC = Files.readAllBytes(Paths.get("key-hmac.txt"));
private transient byte[] keyCiphering = Files.readAllBytes(Paths.get("key-ciphering.txt"));

//Load issuer ID from configuration text file
private transient String issuerID = Files.readAllLines(Paths.get("issuer-id.txt")).get(0);

//Init token ciphering handler
TokenCipher tokenCipher = new TokenCipher();

Token creation.

//Create the token with a validity of 15 minutes and client context (IP + Browser fingerprint SHA256 digest HEX encoded) information
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(issuerID)
                       .withIssuedAt(now)
                       .withNotBefore(now)
                       .withClaim("clientIP", retrieveClientIP(request))
                       .withClaim("browserFingerprintDigest", browserFingerprintDigest)
                       .withHeader(headerClaims)
                       .sign(Algorithm.HMAC256(keyHMAC));
//Cipher the token
String cipheredToken = tokenCipher.cipherToken(token, keyCiphering);

Token validation.

//Decipher the token
String token = tokenCipher.decipherToken(cipheredToken, keyCiphering);

//Create a verification context for the token
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(this.keyHMAC))
                               .withIssuer(issuerID)
                               .withClaim("clientIP", retrieveClientIP(request))
                               .withClaim("browserFingerprintDigest", browserFingerprintDigest)
                               .build();

//Verify the token, if the verification fail then a exception is throwed
DecodedJWT decodedToken = verifier.verify(token);

Token storage on client side

Symptom

It's occur when a application store the token in a way allowing this one:

  • To be automatically sent by the browser (Cookie storage).
  • To be retrieved even if the browser is restarted (Use of localStorage container).

How to prevent

Store the token using the browser sessionStorage container and add it as a Bearer with JavaScript when calling service.

Implementation example

JavaScript code to store the token after authentication.

/* Handle request for JWT token and local storage*/
function getToken(){
    var login = $("#login").val();
    var password = $("#password").val();
    var fingerprint = generateFingerprint();
    var fingerprintHash = CryptoJS.SHA256(fingerprint);
    var postData = "login=" + encodeURIComponent(login) + "&password=" + encodeURIComponent(password) +"&browserFingerprintDigest="
    + encodeURIComponent(fingerprintHash);

    $.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 Beader when calling a service, for example a service to validate token here.

/* Handle request for JWT token validation */
function validateToken(){
    var token = sessionStorage.getItem("token");
    var fingerprint = generateFingerprint();
    var fingerprintHash = CryptoJS.SHA256(fingerprint);
    var postData = "browserFingerprintDigest=" + encodeURIComponent(fingerprintHash);

    $.ajax({
        url: "/services/validate",
        type: "POST",
        beforeSend: function(xhr) {
            xhr.setRequestHeader("Authorization", "bearer " + token);
        },
        data: postData,
        success: function(data) {
		...
        },
        error: function(jqXHR, textStatus, error) {
		...
        },
    });
}

Authors and Primary Editors

Dominique Righetto - [email protected]

Other Cheatsheets