Search code examples
javascriptnode.jstypescriptjwt

JSON Web Token (JWT) Error: Invalid Signature with RSA Key Pairs


I'm encountering an issue in my Node.js (20.5.1) application related to JSON Web Token (JWT) verification using RSA key pairs. The error message is as follows:

[16:39:56.959] FATAL (26460): invalid signature
err: {
  "type": "JsonWebTokenError",
  "message": "invalid signature",
  "stack":
      JsonWebTokenError: invalid signature
          at U:\Coding\MCShop-API\node_modules\jsonwebtoken\verify.js:171:19
          at getSecret (U:\Coding\MCShop-API\node_modules\jsonwebtoken\verify.js:97:14)
          at module.exports (U:\Coding\MCShop-API\node_modules\jsonwebtoken\verify.js:101:10)
          at verifyJWTToken (U:\Coding\MCShop-API\src\crypto.ts:28:37)
          at U:\Coding\MCShop-API\src\app.ts:39:45
          at Layer.handle [as handle_request] (U:\Coding\MCShop-API\node_modules\express\lib\router\layer.js:95:5)
          at trim_prefix (U:\Coding\MCShop-API\node_modules\express\lib\router\index.js:328:13)
          at U:\Coding\MCShop-API\node_modules\express\lib\router\index.js:286:9
          at Function.process_params (U:\Coding\MCShop-API\node_modules\express\lib\router\index.js:346:12)
          at next (U:\Coding\MCShop-API\node_modules\express\lib\router\index.js:280:10)
  "name": "JsonWebTokenError"
}

I have also attached the crypto.ts file that handles the JSON Web Tokens for my application.

import crypto from 'crypto';
import { readFileSync } from 'fs';
import { JwtPayload, sign, verify } from 'jsonwebtoken';
import { logger } from './app';

export function generateRSAKeyPair() {
    const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
        modulusLength: 512,
        publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
        privateKeyEncoding: { type: 'pkcs1', format: 'pem' }
    });

    return { privateKey, publicKey };
}

export function generateJWTToken(admin: boolean, username: string) {
    const key = readFileSync('private.key', { encoding: 'utf-8', flag: 'r' });
    return sign({
        admin,
        username
    }, key, { algorithm: 'RS256' });
}

export function verifyJWTToken(token: string) {
    try {
        const key = readFileSync('public.key', { encoding: 'utf-8', flag: 'r' });
        const verifiedToken = verify(token, key, { algorithms: ['RS256'] }) as JwtPayload;
        if (!verifiedToken) return false;
        return verifiedToken

    } catch (error) {
        logger.fatal(error);
        return false;
    }
}

I have confirmed the following:

  • The key variable is not undefined and is fetching the contents of the file.
  • readFileSync does not use caching.
  • The values that get passed into the function are valid.
  • The attempted JWT is indeed valid confirmed by JWT.io jwt.io

I suspect there might be an error in how I'm handling the keys or in the JWT library version.

Can someone help me identify the root cause of the "invalid signature" error and suggest potential solutions? Any insights or advice would be greatly appreciated.


Solution

  • I ran your code and it was fine. I had to change the modulusLength, which is the key size in bits. For RS256 you would therefore set it to 256 * 8 = 2048. You should be able to copy the 3 below files into a folder, then run these commands:

    npm install
    npm start
    

    index.ts

    import crypto from 'crypto';
    import { readFileSync, writeFileSync } from 'fs';
    import { JwtPayload, sign, verify } from 'jsonwebtoken';
    
    export function generateRSAKeyPair() {
        const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
            modulusLength: 2048,
            publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
            privateKeyEncoding: { type: 'pkcs1', format: 'pem' }
        });
    
        return { privateKey, publicKey };
    }
    
    export function generateJWTToken(admin: boolean, username: string) {
        const key = readFileSync('private.key', { encoding: 'utf-8', flag: 'r' });
        return sign({
            admin,
            username
        }, key, { algorithm: 'RS256' });
    }
    
    export function verifyJWTToken(token: string) {
        const key = readFileSync('public.key', { encoding: 'utf-8', flag: 'r' });
        const verifiedToken = verify(token, key, { algorithms: ['RS256'] }) as JwtPayload;
        if (!verifiedToken) return false;
        return verifiedToken
    }
    
    const { privateKey, publicKey } = generateRSAKeyPair();
    writeFileSync('./private.key', privateKey);
    writeFileSync('./public.key', publicKey);
    const token = generateJWTToken(true, 'john doe');
    console.log(token);
    const result = verifyJWTToken(token);
    console.log(`Result is ${JSON.stringify(result)}`)
    

    package.json

    {
      "name": "demo",
      "version": "1.0.0",
      "description": "",
      "main": "index.ts",
      "scripts": {
        "start": "tsx ./index.ts"
      },
      "dependencies": {
        "@types/node": "^20.11.0",
        "jsonwebtoken": "^9.0.2",
        "tsx": "^4.7.0",
        "typescript": "^5.3.3"
      }
    }
    

    tsconfig.json

    {
        "compilerOptions": {
          "strict": true,
          "target": "ES2022",
          "lib": ["ES2022"],
          "module":"ES2022",
          "moduleResolution": "Node",
          "allowSyntheticDefaultImports": true,
          "sourceMap": false
        },
        "include": [
          "*.ts"
        ],
        "exclude": [
          "node_modules"
        ]
    }