JWT tokens in Java using the jjwt-api version 0.11.5 in Eclipse with use of Keys.hmacShaKeyFor

Hi there! check out my previous blog How Keys.hmacShaKeyFor() works ? It will you brief idea.

Find other JWT blogs within label JWT (https://nurturedknowledge.blogspot.com/search/label/JWT?&max-results=9)

Also check it out this blog which uses Keys.secretKeyFor() method to generate security.
Here is the link: 
JWT tokens in Java using the jjwt-api version 0.11.5 in Eclipse

Let's write program to use a user-defined security key and then use it with Keys.hmacShaKeyFor().

package jwtdemo;


import io.jsonwebtoken.Claims;

import io.jsonwebtoken.Jwts;

import io.jsonwebtoken.SignatureAlgorithm;

import io.jsonwebtoken.security.Keys;


import java.security.Key;

import java.util.Base64;

import java.util.Date;

import java.util.UUID;


public class JwtUtil1{


// User-defined secret key (should be stored securely in a real application)

private static final String USER_SECRET = "your-secret-key-here-replace-me";


// Encode the user-defined secret key in Base64

private static final String BASE64_SECRET = Base64.getEncoder().encodeToString(USER_SECRET.getBytes());


// Generate the signing key from the Base64 encoded secret

private static final Key SIGNING_KEY = Keys.hmacShaKeyFor(Base64.getDecoder().decode(BASE64_SECRET));


// Method to generate a JWT token

public static String generateToken(String subject) {

Date now = new Date();

Date expiryDate = new Date(now.getTime() + 3600000); // Token expires in 1 hour


return Jwts.builder()

.setId(UUID.randomUUID().toString()) // Unique identifier for the token

.setSubject(subject) // Who the token is for

.setIssuedAt(now) // When the token was issued

.setExpiration(expiryDate) // When the token expires

.signWith(SIGNING_KEY) // Sign the token with our derived signing key

.compact(); // Serialize the claims and generate the JWT

}


// Method to parse and validate a JWT token

public static Claims parseAndValidateToken(String token) {

try {

return Jwts.parserBuilder()

.setSigningKey(SIGNING_KEY) // Use the same signing key for validation

.build()

.parseClaimsJws(token) // Parse the token and verify the signature

.getBody(); // Get the claims from the parsed token

} catch (Exception e) {

// Token is invalid or has expired

return null;

}

}


public static void main(String[] args) {

// Example usage:


// 1. Generate a token for a user

String username = "anotherUser";

String token = generateToken(username);

System.out.println("Generated Token (using user-defined key): " + token);


// 2. Parse and validate the token

Claims claims = parseAndValidateToken(token);


if (claims != null) {

System.out.println("\n JWTUtil-1 is called");

System.out.println("\nToken is valid!");

System.out.println("Subject: " + claims.getSubject());

System.out.println("Issued At: " + claims.getIssuedAt());

System.out.println("Expiration: " + claims.getExpiration());

System.out.println("Token ID: " + claims.getId());

} else {

System.out.println("\nToken is invalid or has expired.");

}


// 3. Try to parse a token generated with a different (default) key

Key defaultKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);

String tokenWithDefaultKey = Jwts.builder()

.setSubject("test")

.signWith(defaultKey)

.compact();


Claims claimsFromDefaultKey = parseAndValidateToken(tokenWithDefaultKey);

if (claimsFromDefaultKey == null) {

System.out.println("\nToken generated with a different key is correctly identified as invalid.");

} else {

System.out.println("\nToken generated with a different key was surprisingly valid!");

}

}

}


Explanation of Changes:

  1. private static final String USER_SECRET = "your-secret-key-here-replace-me";:

    • We've introduced a USER_SECRET constant where you can define your own secret key as a String. Remember to replace "your-secret-key-here-replace-me" with a strong and securely generated secret key in a real application.
  2. private static final String BASE64_SECRET = Base64.getEncoder().encodeToString(USER_SECRET.getBytes());:

    • We use java.util.Base64.getEncoder().encodeToString() to encode the USER_SECRET String into its Base64 representation. The .getBytes() method is used to get the byte array of the secret key String before encoding.
  3. private static final Key SIGNING_KEY = Keys.hmacShaKeyFor(Base64.getDecoder().decode(BASE64_SECRET));:

    • Here's the crucial part:
      • Base64.getDecoder().decode(BASE64_SECRET): We first decode the Base64 encoded string back into its original byte array.
      • Keys.hmacShaKeyFor(...): We then pass this decoded byte array to Keys.hmacShaKeyFor(). This method now uses the bytes from your user-defined key to create the java.security.Key object suitable for HMAC-SHA signing. The jjwt library internally handles the requirements for the key length based on the algorithm it will be used with (which is implied when you use signWith later).
  4. signWith(SIGNING_KEY) and setSigningKey(SIGNING_KEY):

    • In both the generateToken and parseAndValidateToken methods, we now use the SIGNING_KEY that was derived from your user-defined secret key.

How it Works:

  • You provide a String as your secret key.
  • This String is encoded into a Base64 String. Base64 encoding is often used to represent binary data in a text format.
  • When we need to use the key for JWT signing and verification, we first decode the Base64 String back into its original byte array.
  • The Keys.hmacShaKeyFor() method then takes these raw bytes and creates the java.security.Key object that jjwt needs for its cryptographic operations.

Important Security Notes:

  • String Encoding: While we're encoding your secret key to Base64 here, this is primarily for demonstration. Base64 is an encoding scheme, not an encryption method. It doesn't provide any additional security. The security of your JWTs still relies entirely on the strength and secrecy of your original USER_SECRET.
  • Key Length: Ensure your USER_SECRET is sufficiently long for the HMAC-SHA algorithm you intend to use (at least 32 bytes for HS256, 48 bytes for HS384, and 64 bytes for HS512). Shorter keys are easier to brute-force.
  • Secure Storage: In a real application, do not hardcode your secret key directly in the code like this example. Store it securely using environment variables, configuration files with restricted access, or a dedicated secret management service.

Now, when you run this updated JwtUtil class in Eclipse, it will generate and validate JWTs using the security key you defined in the USER_SECRET constant (after it's been Base64 encoded and then decoded for use with Keys.hmacShaKeyFor()). Remember to replace the placeholder secret with a strong one!

Let's break down the "why" behind encoding the secret key to Base64 and then immediately decoding it:

  1. Keys.hmacShaKeyFor() Expects Raw Bytes: The Keys.hmacShaKeyFor() method in the jjwt library is designed to work with the raw byte representation of the secret key. It takes a byte[] as input (implicitly, as it handles the key material internally).

  2. User-Defined Key as a String: In our example, we started with a user-defined secret key as a String:

    private static final String USER_SECRET = "your-secret-key-here-replace-me";

    Strings in Java are sequences of characters, and when we want to use them for cryptographic operations, we often need their underlying byte representation.

  3. Base64 Encoding for Representation (Often from External Sources): Base64 encoding is a common way to represent binary data (like cryptographic keys) as ASCII strings. This is particularly useful when:

    • Storing keys in configuration files: Text-based configuration files are easier to manage.
    • Transmitting keys over text-based channels: Environment variables or system properties often store values as strings.
    • Human readability (to some extent): While not truly human-readable in a meaningful way for security, Base64 is a standard encoding that developers are familiar with.

    So, if you were to receive a secret key from an external source (e.g., an environment variable) it might very well be in Base64 encoded format.

  4. The Conversion Process:

    • Encoding (Base64.getEncoder().encodeToString(USER_SECRET.getBytes())): We first convert our USER_SECRET String into a byte array using .getBytes(). Then, we encode these bytes into a Base64 String. At this point, BASE64_SECRET holds the Base64 representation.

    • Decoding (Base64.getDecoder().decode(BASE64_SECRET)): Immediately in the next step, we take this BASE64_SECRET String and decode it back into its original byte array using Base64.getDecoder().decode().

  5. Why the Decode Before hmacShaKeyFor()? The crucial reason for the decoding step is that Keys.hmacShaKeyFor() needs the raw bytes of the secret key to properly initialize the cryptographic key object. It doesn't directly accept a Base64 encoded String.

Think of it like this:

Imagine you have a valuable ingredient (the secret key) for a special recipe (JWT signing).

  • Sometimes, this ingredient might be delivered to you in a special package (Base64 encoding) for easier handling or storage.
  • Before you can actually use the ingredient in your recipe, you need to take it out of the package (decode it) to get the raw ingredient.
  • Keys.hmacShaKeyFor() is like the part of your recipe that requires the raw ingredient.

In our code:

We might have chosen to represent our USER_SECRET as a plain String in the code for simplicity in this example. However, the pattern of encoding and then immediately decoding demonstrates how you would handle a secret key that was provided or stored in a Base64 encoded format.

Alternative Approach (Without Explicit Encoding/Decoding in Code):

If you were directly providing the raw bytes of your secret key (which is less common when defining it directly in code as a constant), you wouldn't need the Base64 encoding and decoding steps:

// Raw byte representation of the secret key (less readable and harder to manage directly)
private static final byte[] RAW_SECRET_BYTES = new byte[]{/* some byte values */};
private static final Key SIGNING_KEY_RAW = Keys.hmacShaKeyFor(RAW_SECRET_BYTES);

However, managing raw byte arrays directly in code is often less convenient than dealing with Strings, especially for configuration. This is why the Base64 encoding/decoding pattern is frequently seen when working with keys that are initially represented as Strings.

In summary, the encoding to Base64 and immediate decoding is done to:

  • Handle the scenario where the secret key might be provided or stored in a Base64 encoded format (a common practice).
  • Convert the String representation of the key into the raw byte array that Keys.hmacShaKeyFor() requires to create the java.security.Key object.

It might seem like an extra step in our specific example where we define the USER_SECRET directly as a String, but it illustrates a common pattern for dealing with secret keys in real-world applications.


Post a Comment

0 Comments