且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

如何使用在线工具手动验证 JWT 签名

更新时间:2023-09-02 09:14:28

都是格式和编码的问题.

https://jwt.io 上,您会根据您的输入值和密码获得此令牌:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFa

我们要证明签名:

3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

是正确的.

签名是经过 Base64url 编码的 HMAC-SHA256 哈希.(如 RFC7515 中所述)

当您使用 在线 HMAC 生成器 计算哈希时对于

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

秘密

hONPMX3tHWIp9jwLDtoCUwFAtH0RwSK6

你得到

de921a2a4b225fd66ff0983e8566eb0f6e1584bdfa84120568da40e1f571dbd3

作为结果,这是一个 HMAC-SHA256 值,但不是 Base64url 编码的.这个哈希是一个大数字的十六进制字符串表示.

要将其与 https://jwt.io 中的值进行比较,您需要将值转换为十六进制字符串表示回一个数字,然后 Base64url 对其进行编码.

以下脚本正在执行此操作,并且还使用 crypto-js 来计算它自己的哈希值.这也可以是您在没有 JWT 库的情况下进行验证的一种方式.

var CryptoJS = require("crypto-js");//输入值var base64Header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";var base64Payload = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ";var secret = "hONPMX3tHWIp9jwLDtoCUwFAtH0RwSK6";//来自不同在线工具的两个哈希var signatureJWTIO = "3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M";var onlineCaluclatedHS256 = "de921a2a4b225fd66ff0983e8566eb0f6e1584bdfa84120568da40e1f571dbd3";//使用 Crypto-JS 进行哈希计算.//两个replace表达式通过replace将Base64转换为Base64url格式//'+' 和 '-','/' 和 '_' 并去除 '=' 填充var base64Signature = CryptoJS.HmacSHA256(base64Header + "." + base64Payload , secret).toString(CryptoJS.enc.Base64).replace(/+/g,'-').replace(///g,'_').replace(/=+$/m,'');//将在线计算的值转换为 Base64 表示var base64hash = new Buffer.from(onlineCaluclatedHS256, 'hex').toString('base64').replace(///g,'_').replace(/+/g,'-').replace(/=+$/m,'')//结果:console.log("来自 JWT.IO 的签名:" + signatureJWTIO);console.log("NodeJS 计算哈希:" + base64Signature);console.log("在线计算哈希(转换):"+base64hash);

结果是:

来自 JWT.IO 的签名:3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29MNodeJS计算哈希:3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M在线计算哈希(转换):3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

一模一样!

结论:

不同在线工具计算的值都是正确的,但由于格式和编码不同,无法直接比较.如上所示的一个小脚本可能是一个更好的解决方案.

From what I can understand, it's a straight forward process to validate a JWT signature. But when I use some online tools to do this for me, it doesn't match. How can I manually validate a JWT signature without using a JWT library? I'm needing a quick method (using available online tools) to demo how this is done.

I created my JWT on https://jwt.io/#debugger-io with the below info:

  • Algorithm: HS256
  • Secret: hONPMX3tHWIp9jwLDtoCUwFAtH0RwSK6
  • Header:
    {
      "alg": "HS256",
      "typ": "JWT"
    }
    

  • Payload:
    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    

  • Verify Signature (section):
    • Secret value changed to above
    • "Checked" secret base64 encoded (whether this is checked or not, still get a different value)

JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.wDQ2mU5n89f2HsHm1dluHGNebbXeNr748yJ9kUNDNCA


Manual JWT signature verification attempt:

Using a base64UrlEncode calculator (http://www.simplycalc.com/base64url-encode.php or https://www.base64encode.org/)

If I: (Not actual value on sites, modified to show what the tools would ultimately build for me)

base64UrlEncode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9") + "." + base64UrlEncode("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ")

I get:

ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5.ZXlKemRXSWlPaUl4TWpNME5UWTNPRGt3SWl3aWJtRnRaU0k2SWtwdmFHNGdSRzlsSWl3aWFXRjBJam94TlRFMk1qTTVNREl5ZlE=

NOTE: there's some confusion on my part if I should be encoding the already encoded values, or use the already encoded values as-is.

(i.e. using base64UrlEncode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9") + "." + base64UrlEncode("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ") vs "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ").

Regardless on which I should do, the end result still doesn't match the signature. I'm leaning towards that I should NOT re-encode the encoded value, whether that's true or not.

Then using a HMAC Generator calculator (https://codebeautify.org/hmac-generator or https://www.freeformatter.com/hmac-generator.html#ad-output)

(Not actual value on sites, modified to show what the tools would ultimately build for me)

HMACSHA256(
 "ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5.ZXlKemRXSWlPaUl4TWpNME5UWTNPRGt3SWl3aWJtRnRaU0k2SWtwdmFHNGdSRzlsSWl3aWFXRjBJam94TlRFMk1qTTVNREl5ZlE=",
  "hONPMX3tHWIp9jwLDtoCUwFAtH0RwSK6"
)

Which gets me:

a2de322575675ba19ec272e83634755d4c3c2cd74e9e23c8e4c45e1683536e01

And that doesn't match the signature portion of the JWT:

wDQ2mU5n89f2HsHm1dluHGNebbXeNr748yJ9kUNDNCAM != a2de322575675ba19ec272e83634755d4c3c2cd74e9e23c8e4c45e1683536e01


Purpose:

The reason I'm needing to confirm this is to prove the ability to validate that the JWT hasn't been tampered with, without decoding the JWT.

My clients web interface doesn't need to decode the JWT, so there's no need for them to install a jwt package for doing that. They just need to do a simple validation to confirm the JWT hasn't been tampered with (however unlikely that may be) before they store the JWT for future API calls.

It's all a matter of formats and encoding.

On https://jwt.io you get this token based on your input values and secret:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

We want to prove that the signature:

3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

is correct.

The signature is a HMAC-SHA256 hash that is Base64url encoded. (as described in RFC7515)

When you use the online HMAC generator to calculate a hash for

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

with the secret

hONPMX3tHWIp9jwLDtoCUwFAtH0RwSK6

you get

de921a2a4b225fd66ff0983e8566eb0f6e1584bdfa84120568da40e1f571dbd3

as result, which is a HMAC-SHA256 value, but not Base64url encoded. This hash is a hexadecimal string representation of a large number.

To compare it with the value from https://jwt.io you need to convert the value from it's hexadecimal string representation back to a number and Base64url encode it.

The following script is doing that and also uses crypto-js to calculate it's own hash. This can also be a way for you to verify without JWT libraries.

var CryptoJS = require("crypto-js");

// the input values
var base64Header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
var base64Payload = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ";
var secret = "hONPMX3tHWIp9jwLDtoCUwFAtH0RwSK6";

// two hashes from different online tools
var signatureJWTIO = "3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M";
var onlineCaluclatedHS256 =  "de921a2a4b225fd66ff0983e8566eb0f6e1584bdfa84120568da40e1f571dbd3";

// hash calculation with Crypto-JS. 
// The two replace expressions convert Base64 to Base64url format by replacing 
// '+' with '-', '/' with '_' and stripping the '=' padding
var base64Signature = CryptoJS.HmacSHA256(base64Header + "." + base64Payload , secret).toString(CryptoJS.enc.Base64).replace(/+/g,'-').replace(///g,'_').replace(/=+$/m,'');

// converting the online calculated value to Base64 representation
var base64hash = new Buffer.from(onlineCaluclatedHS256, 'hex').toString('base64').replace(///g,'_').replace(/+/g,'-').replace(/=+$/m,'')


// the results:
console.log("Signature from JWT.IO             : " + signatureJWTIO);
console.log("NodeJS calculated hash            : " + base64Signature);
console.log("online calulated hash (converted) : " + base64hash);

The results are:

Signature from JWT.IO             : 3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

NodeJS calculated hash            : 3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

online calulated hash (converted) : 3pIaKksiX9Zv8Jg-hWbrD24VhL36hBIFaNpA4fVx29M

identical!

Conclusion:

The values calculated by the different online tools are all correct but not directly comparable due to different formats and encodings. A little script as shown above might be a better solution.