node-jsonwebtoken: "JsonWebTokenError: invalid signature" when verifying JWT signed with Java JWT

I use https://github.com/jwtk/jjwt to encode and sign a token as follow:

    Jwts.builder()
        .setSubject(authentication.getName())
        .claim(AUTHORITIES_KEY, authorities)
        .signWith(SignatureAlgorithm.HS512, "my-secret-token-to-change-in-production")
        .setExpiration(validity)
        .compact();

then decode in node.js as follow:

var decoded = jwt.verify(req.get('Authorization'), 'my-secret-token-to-change-in-production', { algorithms: ['HS512'] });

and get the error:

JsonWebTokenError: invalid signature

Using jwt.decode I get the token content without problem.

Am I doing something wrong?

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 5
  • Comments: 28

Commits related to this issue

Most upvoted comments

@nodje @DaleWebb @michaelcbarr

I’m not going to take the time to dig into exactly what the java library is doing by default (I don’t work with Java usually, needed to install Eclipse and figure and Maven and things 😃). However, the issue is the string secret. By calling getBytes("UTF-8") on your secret and providing signWith() with the byte[], everything will work out.

package jwt_test.foo;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.io.UnsupportedEncodingException;
import java.util.Date;

/**
 * Hello world!
 *
 */
public class App 
{
    private static String SECRET = "HelloWorld!";

    public static void main( String[] args ) throws UnsupportedEncodingException
    {
        System.out.println( makeToken() );
    }

    private static String makeToken() throws UnsupportedEncodingException {
        return Jwts.builder()
                .setSubject("foo")
                .claim("bar", "baz")
                .signWith(SignatureAlgorithm.HS512, SECRET.getBytes("UTF-8"))
                .compact();
    }
}
'use strict';

const JWT = require('jsonwebtoken');

const SECRET = 'HelloWorld!';
const JAVA_JWT = 'eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJmb28iLCJiYXIiOiJiYXoifQ.1MOXGiwGTFLU7-YMvOe2_q2ZRUHAMCVS7pbnOkRKCFV1HIvY8odBaqWVCQRuT2RUbKtGgA2elFRsuka4K1KP7A';

JWT.verify(JAVA_JWT, SECRET, { algorithms: ['HS512'] })

An author of JJWT here…

FWIW, cryptographic signatures are always computed with byte array keys - never strings. You can get the UTF-8 bytes of a String as demonstrated above, but that only masks what could be a very problematic cryptographic weakness (I’m not saying those in this thread are experiencing that weakness - I’m just raising that this could happen to anyone that might not understand what is going on).

Digital signature keys (again, byte arrays), should ideally never be based on simple strings like ‘my secret’ or ‘my password’. Or, at the very least, if a simple password should be used as a signing key, it is almost always better to send it through a key-derivation algorithm (like PBKDF2) and then use that resulting output as the signature key. This ensures sufficient cryptographic entropy (randomness) that short, human-readable strings don’t (and which are therefore risky).

Signing keys should ideally always be:

  1. generated by a secure-random number generator or at the least created via a cryptographically secure key-derivation function and
  2. and this is important - of sufficient length for the hashing algorithm to be used..

Number 2 is why JJWT provides the MacProvider.generateKey method - to ensure you always have keys of sufficient strength for the algorithm chosen. You can then easily base64 the result:

SecretKey key = MacProvider.generateKey(SignatureAlgorithm.HS256);
String base64Encoded = TextCodec.BASE64.encode(key.getEncoded());

This is why JJWT expects Base64 by default - because if you do these best practices, you’ll always end up with a byte array key (e.g. key.getEncoded()). And if you have a byte array key, the most common way to turn that into a string (e.g. for configuration) is to Base64-encode that byte array.

Finally, note that TextCodec.BASE64.decode(myKey) does NOT produce the same byte array (key) as myKey.getBytes('UTF-8'). The latter is usually incorrect in cryptographic contexts.

That means that my-secret-token-to-change-in-production.getBytes("UTF-8") might represent a weakened signing key, and as a result, shouldn’t be used. I recommend dumping that current key and generating a new one with strong cryptographic guarantees as shown above (e.g. using JJWT) and ensuring your Node library base64-decodes your string correctly.

HTH!

I’m having a similar issue. I’m generating a key elswhere, and trying to validate it using this library. It validates fine in other services using libraries from different languages. Looking at this issue and the answers to it, am i right in assuming that the node library generates a byte array for validating by UTF-8 decoding the supplied secret string? Is there any way to get it to base64url-decode the key (as i believe would be the “proper” way for the reasons described by the previous comment)? I’ve tried having a dig through the code but haven’t come up with anything.

EDIT: Just figured it out. Passing new Buffer(secret, 'base64') instead of the string in did the trick!

@DaleWebb @omsmith

AWESOME WORK!

I can confirm that this works perfectly for me - just need to remember to convert the SECRET string to bytes on Java verify method.

claims = Jwts.parser()
                    .setSigningKey(secret.getBytes("UTF-8"))
                    .parseClaimsJws(token)
                    .getBody();

(Also need to catch the UnsupportedEncodingException!)

Think I would still be scratching my head next month without your help - thanks again @omsmith !

Mb

I’d love to give you a hand with this. Would you be able to provide an example token and the secret you used to sign it so I can take a look. With what you’ve provided, hard to say - from looking at jjwt your example should be throwing since "my-secret-token-to-change-in-production" is not base64.

The solution @mgkeen provided in his edit worked for me.

If this is the appropriate thing to do can someone update the documentation? i.e.

var tokenDecoded = jwt.verify(token, new Buffer(MYSECRET, 'base64'), function(err, decoded) {

hum, the base64 issue sounds like a good lead, but I couldn’t verify the signature with the secret encoded in base64 like this: var decoded = jwt.verify(req.get('Authorization'), new Buffer('my-secret-token-to-change-in-production').toString('base64'), { algorithms: ['HS512'] }); Here’s a token generated by jjwt with my favorite secret my-secret-token-to-change-in-production: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImF1dGgiOiJST0xFX0FETUlOLFJPTEVfVVNFUiIsImV4cCI6MTQ2Nzc3MzQ5Nn0.v0Gfc9fKDQfkDningjmObkD5-EcbfWy5vuvuOimTV032iCoOaaQtCsZxQC78JbLbeQNLUA3UaQnuLgvwwqLmIg

Great! I can confirm that this solution works too.

@ralphgabrielle assuming the latest stable JJWT version, which is 0.10.7 at the time of writing, you can do:

public class App {

    // Note that 'HelloWorld' is not a valid JWT signing key per the JWA RFC's 
    // key strength requirements.  For why, read this for more information:
    // https://stackoverflow.com/a/40274325/407170
    // Change this to something else or derive a key using PBKDF2
    private static String SECRET = "HelloWorld!";

    public static void main( String[] args ) throws UnsupportedEncodingException {
        System.out.println( makeToken() );
    }

    private static String makeToken() throws UnsupportedEncodingException {
        Key key = Keys.secretKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
        return Jwts.builder()
                .setSubject("foo")
                .claim("bar", "baz")
                .signWith(key)
                .compact();
    }
}

@DmanDman are you signing your token just like that? You should use new Buffer( 'ThisStringIsASecret', 'base64' ) to sign your token

@michaelcbarr If I cannot get a solution before we have to ship the feature, I’m going to create a route on the gateway that creates the token to give the information back.

private static String generateToken(Account account, App app) {
        return Jwts.builder()
            .setSubject(account.getId())
            .claim("app_id", app.getSlug())
            .setExpiration(new Date(new Date().getTime() + TOKEN_LIFETIME))
            .signWith(SignatureAlgorithm.HS512, PersonaApplication.getSecret())//the secret is a String, not base64 encoded.
            .compact();
    }

no problem - this was borrowed form a tutorial - can’t find the link but will credit if I find it 😄

var jwt = require('jsonwebtoken');
jwt.verify(token, superSecret, function(err, decoded) {
                if (err) {
                    return res.json({ success: false, message: 'Failed to authenticate token.' });
                } else {
                    // if everything is good, save to request for use in other routes
                    req.decoded = decoded;
                    next();
                }
            });

The token is created in Java with code similar to this:

private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(this.generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
   }