JWT Authentication Java Tutorial

JWT Authentication Java Tutorial

Why Use JWT?

Carrying meaningful data in the token

Bearer Authentication can be random tokens. They are secure and remove the need of jsession id. But they will be more useful if they can carry information along with them.

{
  "sub": "13324",
  "aud": "admin",
  "exp": 1512203223
}

Creating a JWT token

A JWT token has 3 parts to it.

  1. Header - For agreeing on the algorithm for signing the message.
  2. Payload - For carrying user data.
  3. Signature - For Verification

Header and Payload both are JSON. They need to be Base64 encoded. The dot separates each part.

JWT Header Payload and Signature

String signature = hmacSha256(base64(header) + "." + base64(payload), secret);
String jwtToken = base64(header) + "." + base64(payload) + "." + signature;

Here is an example

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0IiwiYXVkIjpbImFkbWluIl0sImlzcyI6Im1hc29uLm1ldGFtdWcubmV0IiwiZXhwIjoxNTc0NTEyNzY1LCJpYXQiOjE1NjY3MzY3NjUsImp0aSI6ImY3YmZlMzNmLTdiZjctNGViNC04ZTU5LTk5MTc5OWI1ZWI4YSJ9.EVcCaSqrSNVs3cWdLt-qkoqUk7rPHEOsDHS8yejwxMw

The standard algorithm for signing is HMAC + SHA256 also called has HS256. Here we will declare the header as a constant string. This header will be used during verification for checking the algorithm used.

private static final String JWT_HEADER = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";

Encoding

We need to encode the header and payload. For encoding, we use Base64 and URL encoding. Java 8 provides a method for Base64 URL encoding.

URL encoding makes the JWT safe to be sent as a part of the url.

import java.util.Base64;

//...

private static String encode(byte[] bytes) {
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}

So encoding the header will always give us constant string eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

Payload

The simplest information you can store in your payload is

  1. User ID (subject)
  2. Role (audience)
  3. Token Expiry Time (expiry)
private JSONObject payload = new JSONObject();

//...

payload.put("sub", sub);
payload.put("aud", aud);
payload.put("exp", expires);

We must encode the payload since it will be in JSON format. After encoding it will become a compact string as follows.

eyJzdWIiOiIxMjM0IiwiYXVkIjpbImFkbWluIl0sImlzcyI6Im1hc29uLm1ldGFtdWcubmV0IiwiZXhwIjoxNTc0NTEyNzY1LCJpYXQiOjE1NjY3MzY3NjUsImp0aSI6ImY3YmZlMzNmLTdiZjctNGViNC04ZTU5LTk5MTc5OWI1ZWI4YSJ9

During verification, this string is decoded using the same Base64 URL decoding mechanism to retrieve the JSON payload

HMAC SHA256 Signature

Header and payload are concatenated with a dot and signed with HMAC + SHA256 algorithm using a secret key. You need to maintain a configurable secret key somewhere.


private String hmacSha256(String data, String secret) {
    try {

        //MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = secret.getBytes(StandardCharsets.UTF_8);//digest.digest(secret.getBytes(StandardCharsets.UTF_8));

        Mac sha256Hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(hash, "HmacSHA256");
        sha256Hmac.init(secretKey);

        byte[] signedBytes = sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));

        return encode(signedBytes);
    } catch (NoSuchAlgorithmException | InvalidKeyException ex) {
        Logger.getLogger(JWebToken.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);
        return null;
    }
}
//...
signature = hmacSha256(encodedHeader + "." + encode(payload));

Read how and why cryptographic hash functions are used to sign messages.

Verification

Token verification does not require any database call.

Token-Based authentication requires a database to create and verify tokens. JWT creation may require access to the database for user details. But verification is all about checking if the server has signed the token and its still valid (looking at the expiry time). Since 99% of the request will comprise of resource access and verification (Rest 1% may be unauthenticated resources access). This makes JWT a more efficient token authentication mechanism.

Verification requires the following steps:

  1. Validity Check
  2. Signature verification

The token received in the request must contain 3 parts we mentioned above. Let us split the parts using String split method.

//split into 3 parts with . delimiter
String[] parts = token.split("\\."); 

All three parts are Base64 url encoded. So use the equivalent decoder.

private static String decode(String encodedString) {
    return new String(Base64.getUrlDecoder().decode(encodedString));
}

We converted the decoded JSON string to JSONObject

JSONObject payload = new JSONObject(decode(parts[1]));

Check if the expiry timestamp is greater than current timestamp

payload.getLong("exp") > (System.currentTimeMillis() / 1000)

Regenerate the signature as explained in earlier step using the same algorithm. Check this regenerated token is matching the signature mentioned in the token.

signature.equals(hmacSha256(parts[0] + "." + parts[1], secret))

If these above two conditions pass, then the token is valid.

Use the information in the payload

Now that we trust the token, we can use the User Id and the role mentioned in the token and provide access to the requested resource. The payload shouldn't contain sensitive information like payment information.

Code

The below program provides a constructor for both generating the token and verifying the generated token. It uses more information like issued at (iat), issuer(iat), etc. Check metamug auth example for more details.


import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 *
 * @author user
 */
public class JWebToken {

    private static final String SECRET_KEY = "FREE_MASON"; //@TODO Add Signature here
    private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
    private static final String ISSUER = "mason.metamug.net";
    private static final String JWT_HEADER = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
    private JSONObject payload = new JSONObject();
    private String signature;
    private String encodedHeader;

    private JWebToken() {
        encodedHeader = encode(new JSONObject(JWT_HEADER));
    }

    public JWebToken(JSONObject payload) {
        this(payload.getString("sub"), payload.getJSONArray("aud"), payload.getLong("exp"));
    }

    public JWebToken(String sub, JSONArray aud, long expires) {
        this();
        payload.put("sub", sub);
        payload.put("aud", aud);
        payload.put("exp", expires);
        payload.put("iat", LocalDateTime.now().toEpochSecond(ZoneOffset.UTC));
        payload.put("iss", ISSUER);
        payload.put("jti", UUID.randomUUID().toString()); //how do we use this?
        signature = hmacSha256(encodedHeader + "." + encode(payload), SECRET_KEY);
    }

    /**
     * For verification
     *
     * @param token
     * @throws java.security.NoSuchAlgorithmException
     */
    public JWebToken(String token) throws NoSuchAlgorithmException {
        this();
        String[] parts = token.split("\\.");
        if (parts.length != 3) {
            throw new IllegalArgumentException("Invalid Token format");
        }
        if (encodedHeader.equals(parts[0])) {
            encodedHeader = parts[0];
        } else {
            throw new NoSuchAlgorithmException("JWT Header is Incorrect: " + parts[0]);
        }

        payload = new JSONObject(decode(parts[1]));
        if (payload.isEmpty()) {
            throw new JSONException("Payload is Empty: ");
        }
        if (!payload.has("exp")) {
            throw new JSONException("Payload doesn't contain expiry " + payload);
        }
        signature = parts[2];
    }

    @Override
    public String toString() {
        return encodedHeader + "." + encode(payload) + "." + signature;
    }

    public boolean isValid() {
        return payload.getLong("exp") > (LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)) //token not expired
                && signature.equals(hmacSha256(encodedHeader + "." + encode(payload), SECRET_KEY)); //signature matched
    }

    public String getSubject() {
        return payload.getString("sub");
    }

    public List<String> getAudience() {
        JSONArray arr = payload.getJSONArray("aud");
        List<String> list = new ArrayList<>();
        for (int i = 0; i < arr.length(); i++) {
            list.add(arr.getString(i));
        }
        return list;
    }

    private static String encode(JSONObject obj) {
        return encode(obj.toString().getBytes(StandardCharsets.UTF_8));
    }

    private static String encode(byte[] bytes) {
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }

    private static String decode(String encodedString) {
        return new String(Base64.getUrlDecoder().decode(encodedString));
    }

    /**
     * Sign with HMAC SHA256 (HS256)
     *
     * @param data
     * @return
     * @throws Exception
     */
    private String hmacSha256(String data, String secret) {
        try {

            //MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = secret.getBytes(StandardCharsets.UTF_8);//digest.digest(secret.getBytes(StandardCharsets.UTF_8));

            Mac sha256Hmac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(hash, "HmacSHA256");
            sha256Hmac.init(secretKey);

            byte[] signedBytes = sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return encode(signedBytes);
        } catch (NoSuchAlgorithmException | InvalidKeyException ex) {
            Logger.getLogger(JWebToken.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);
            return null;
        }
    }

}