Search code examples
jwtlanguage-agnostic

Manually calculating JWT signature never outputs the real signature


From what I understand, this is what I should do to calculate a token:

header =
{
  "alg": "HS256",
  "typ": "JWT"
}

payload =
{
  "sub": "1234567890",
  "name": "JohnDoe",
  "iat": 1516239022
}
secret = "test123"
  1. Remove unnecessary spaces and breaklines from header and payload and then encoding both to base64url.

    base64urlEncode(header)
    // output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    base64urlEncode(payload)
    // output: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG5Eb2UiLCJpYXQiOjE1MTYyMzkwMjJ9
    

Same output as on jwt.io, perfect.

  1. Calculate the sha256 hmac using "test123" as secret.

    sha256_hmac("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG5Eb2UiLCJpYXQiOjE1MTYyMzkwMjJ9", "test123")
    // output: 3b59324118bcd59a5435194120c2cfcb7cf295f25a79149b79145696329ffb95
    
  2. Convert the hash to string and then base64url encode it.

    I use hex to string converter for this part, then I encode it using base64urlEncode and I get the following output:

    O1kyQRjCvMOVwppUNRlBIMOCw4_Di3zDssKVw7JaeRTCm3kUVsKWMsKfw7vClQ
    

    Output from jwt.io:

    O1kyQRi81ZpUNRlBIMLPy3zylfJaeRSbeRRWljKf-5U
    

But if I go to this page From Hex, to Base64 I get the correct output:

O1kyQRi81ZpUNRlBIMLPy3zylfJaeRSbeRRWljKf-5U

So what am I doing wrong? Why converting the hex to string and then Encoding it outputs a different result?

In case the online hex to string conversion is wrong, how can I convert this hex to string (so then I can encode it) on c++ without using any libray. Am I correct if I convert each byte (2 characters because hex = 4 bits) to ASCII character and then encode?


Solution

  • Your hmac step is correct, does have the right output bytes (as commented). The conversion problem you have is caused by non-display chars in the temporary string (the raw bytes were not correctly copied pasted from first webpage to second).

    To reproduce the exact output at each stage, you can use these commands below.

    In terms of C++, you should try to operate on the raw bytes, rather than on the hex string. Take the raw bytes and run them through a base64 URL-safe encoder. Or, as in the example below, take the raw bytes, run them through a plain base64 encoder, and then fix the generated base64 string to be URL safe.

    1. Construct the header
    jwt_header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | base64 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//)
    
    # ans: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    
    1. Construct the payload
    payload=$(echo -n '{"sub":"1234567890","name":"JohnDoe","iat":1516239022}' | base64 | sed s/\+/-/g |sed 's/\//_/g' |  sed -E s/=+$//)
    
    # ans: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG5Eb2UiLCJpYXQiOjE1MTYyMzkwMjJ9
    
    1. Raw password
    secret="test123"
    
    1. Convert secret to hex (not base64)
    hexsecret=$(echo -n "$secret" | xxd -p | tr -d '\n')
    
    # ans: 74657374313233
    
    1. Perform hmac, and capture the raw bytes (caution, this is a non printable string)
    hmac_signature_rawbytes=$(echo -n "${jwt_header}.${payload}" |  openssl dgst -sha256 -mac HMAC -macopt hexkey:$hexsecret -binary)
    
    1. Dump the raw bytes as hex, for illustration only (matches OP output)
    echo -n ${hmac_signature_rawbytes} | xxd -p | tr -d '\n'
    
    #ans: 3b59324118bcd59a5435194120c2cfcb7cf295f25a79149b79145696329ffb95
    
    1. For JWT signature, convert raw bytes to base64uri encoding
    hmac_signature=$(echo -n ${hmac_signature_rawbytes} | base64  | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//)
    
    #ans: O1kyQRi81ZpUNRlBIMLPy3zylfJaeRSbeRRWljKf-5U
    
    1. Create the full token
    jwt="${jwt_header}.${payload}.${hmac_signature}"
    
    # ans: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG5Eb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.O1kyQRi81ZpUNRlBIMLPy3zylfJaeRSbeRRWljKf-5U