javajjwt

java.lang.IllegalArgumentException: A signing key must be specified if the specified JWT is digitally signed


I'm looking to implement JWT in my application for that I'm doing some R&D on it by taking a reference from : https://stormpath.com/blog/jwt-java-create-verify. I was successfully able to implement the generateToken() method, when I am trying to verifyToken() by extracting claim sets. I dont understand from where apiKey.getSecret() is came from. Could you please guide me on this?

The code below for reference:

public class JJWTDemo {

    private static final String secret = "MySecrete";

    private static String generateToken(){
        String id = UUID.randomUUID().toString().replace("-", "");
        Date now = new Date();
        Date exp = new Date(System.currentTimeMillis() + (1000*30)); // 30 seconds

        String token = Jwts.builder()
                .setId(id)
                .setIssuedAt(now)
                .setNotBefore(now)
                .setExpiration(exp)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();

        return token;
    }

    private static void verifyToken(String token){
        Claims claims = Jwts.parser().
                setSigningKey(DatatypeConverter.parseBase64Binary(apiKey.getSecret()))
                .parseClaimsJws(token).getBody();
        System.out.println("----------------------------");
        System.out.println("ID: " + claims.getId());
        System.out.println("Subject: " + claims.getSubject());
        System.out.println("Issuer: " + claims.getIssuer());
        System.out.println("Expiration: " + claims.getExpiration());
    }

    public static void main(String[] args) {
        System.out.println(generateToken());
        String token = generateToken();
        verifyToken(token);
    }
}

I see the below error is coming:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4N2E5NmYwNTcyN2M0ZDY0YjZmODlhNDAyOTQ2OTZiNyIsImlhdCI6MTQ4NDQ4NjYyNiwibmJmIjoxNDg0NDg2NjI2LCJleHAiOjE0ODQ0ODY2NTZ9.ycS7nLWnPpe28DM7CcQYBswOmMUhBd3wQwfZ9C-yQYs
Exception in thread "main" java.lang.IllegalArgumentException: A signing key must be specified if the specified JWT is digitally signed.
    at io.jsonwebtoken.lang.Assert.notNull(Assert.java:85)
    at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:331)
    at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:481)
    at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:541)
    at io.jsonwebtoken.jjwtfun.service.JJWTDemo.verifyToken(JJWTDemo.java:31)
    at io.jsonwebtoken.jjwtfun.service.JJWTDemo.main(JJWTDemo.java:41)

Maven dependency:

<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
<jjwt.version>0.7.0</jjwt.version>

Solution

  • apiKey.getSecret() in the blog article is a reference to the secure, randomly-generated & Base64-encoded secret key (like a password) assigned to the API Key that Stormpath provides every customer. Stormpath customers use this API key to authenticate every request into the Stormpath REST API. Because every Stormpath customer has an API Key (and the key is accessible to your application), the API Key secret is an ideal 'default' for signing and verifying JWTs specific to your application.

    If you don't have a Stormpath API Key, any sufficiently strong secure-random byte array will be just fine for signing and verifying JWTs.

    In your above example, the following is shown as a test key:

    private static final String secret = "MySecrete";
    

    This is not a valid (JWT-compliant) key, and it cannot be used for JWT HMAC algorithms.

    The JWT RFC requires that you MUST use a byte array key length equal to or greater than the hash output length.

    This means that if you use HS256, HS384, or HS512, your key byte arrays must be 256 bits (32 bytes), 384 bits (48 bytes), or 512 bits (64 bytes) respectively. I go into more detail on this in another StackOverflow answer - see the data there, and the MacProvider examples that can generate you a spec-compliant and secure key.

    Based on this, here is that code sample, rewritten to a) generate a valid key and b) reference that key as a Base64-encoded string:

    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import io.jsonwebtoken.impl.crypto.MacProvider;
    
    import java.security.Key;
    import java.util.Base64;
    import java.util.Date;
    import java.util.UUID;
    
    public class JJWTDemo {
    
        private static final Key secret = MacProvider.generateKey(SignatureAlgorithm.HS256);
        private static final byte[] secretBytes = secret.getEncoded();
        private static final String base64SecretBytes = Base64.getEncoder().encodeToString(secretBytes);
    
        private static String generateToken() {
            String id = UUID.randomUUID().toString().replace("-", "");
            Date now = new Date();
            Date exp = new Date(System.currentTimeMillis() + (1000 * 30)); // 30 seconds
    
            String token = Jwts.builder()
                .setId(id)
                .setIssuedAt(now)
                .setNotBefore(now)
                .setExpiration(exp)
                .signWith(SignatureAlgorithm.HS256, base64SecretBytes)
                .compact();
            
            return token;
        }
    
        private static void verifyToken(String token) {
            Claims claims = Jwts.parser()
                .setSigningKey(base64SecretBytes)
                .parseClaimsJws(token).getBody();
            System.out.println("----------------------------");
            System.out.println("ID: " + claims.getId());
            System.out.println("Subject: " + claims.getSubject());
            System.out.println("Issuer: " + claims.getIssuer());
            System.out.println("Expiration: " + claims.getExpiration());
        }
    
        public static void main(String[] args) {
            System.out.println(generateToken());
            String token = generateToken();
            verifyToken(token);
        }
    }
    

    Note that Base64-encoded byte arrays are not encrypted (text encoding != encryption), so ensure that if you Base64-encode your secret key bytes that you still keep that Base64 string safe/hidden.

    Finally, the above static final constants (named secret, secretBytes and base64SecretBytes) are there for this simple test demonstration only - one should never hard code keys into source code, let alone make them static constants, as they can easily be decompiled.