Private key invalid? Testing on jwt.io/#debugger-io?

TLDR
jwt.io will not generate a JWT for my private key?!?

On jwt.io, I navigated over to the “Debugger”.
Then I…

  • I changed the algorithm to ES256
  • I added my private key seen below
  • Added header and payload seen below

(DON’T WORRY, THIS KEY IS NOW DELETED ON coinbase)

-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIFP84NI7eJwHDoCL6HN4fKpXdSlDPY6GXAa5mlJh/vwtoAoGCCqGSM49
AwEHoUQDQgAEaXMCwzBuy+TTNuxxj7FnSA++ZNcbh8B4x1cYmsjSZ2CUj96uYGfV
QQyjxCuFDRa12gtlnG11Ok24xomP7bhgXw==
-----END EC PRIVATE KEY-----

Header

{
  "alg": "ES256",
  "kid": "organizations/cbafb4b3-3756-48fd-adb6-d3ac5c395fd7/apiKeys/1318daa0-5b1f-4d24-bd83-1f5ba72dedc3",
  "nonce": "8ab206ff2b3b1464",
  "typ": "JWT"
}

Payload

{
  "aud": "retail_rest_api_proxy",
  "exp": 1708992742,
  "iat": 1708992622,
  "iss": "coinbase_cloud",
  "nbf": 1708992622,
  "sub": "organizations/cbafb4b3-3756-48fd-adb6-d3ac5c395fd7/apiKeys/1318daa0-5b1f-4d24-bd83-1f5ba72dedc3",
  "uri": "GET api.coinbase.com/api/v3/brokerage/accounts"
}

But jwt.io gives me an “Invalid Signature” error and does not output an encoded jwt.

Is there something wrong with my key? I’ve tried two different keys now with the same issue.

If you care
I’ve been really struggling to properly sign a JWT token with C++. I first attempted creating a jwt using Qt and QMessageAuthenticationCode::hash, using HMAC SHA256. But Coinbase Cloud trading keys need ES256. (I found that out here).

I’ve finally gotten something together using the jwt-cpp library, using openssl. I’m now successfully generating a token, but I’m still failing to authenticate with coinbase. I went to jwt.io to confirm that my signature is correct…

…I’d paste my generated jwt in. It would correctly fill out the HEADER and PAYLOAD sections. I then would put in my private key, but kept getting “Invalid Signature” errors.

So I decided that I’d just try jwt.io to generate a token so that I can see if even a curl command would authenticate me with coinbase…

curl -H "Authorization: Bearer JWT_TOKEN" 'https://api.coinbase.com/api/v3/brokerage/accounts'

But jwt.io would not even create a token for me…

…and that brings us to the TLDR :point_up_2:

I would suggest that you verify your key with their Python SDK! I don’t remember details, but I had “fun” with jwt.io debugger.

I’d prefer to verify it and try to generate a successful (authenticating) token with a tool not associated with coinbase so that I can trust that coinbase is following JWT standards. I would assume that Coinbase’s SDK would successfully work.

As I said, I had “fun” with jwt.io… There are no problems with generated private key from Coinbase!

Get your API keys, both public and private keys.
Use Python SDK to get valid JWT token.
Paste generated token in jwt.io and paste your public key in public key field. Enjoy invalid signature!

If you want to see valid signature message you will need to modify your public key! Change it from:

-----BEGIN EC PUBLIC KEY-----
XXX
-----END EC PUBLIC KEY-----

To:

-----BEGIN PUBLIC KEY-----
XXX
-----END PUBLIC KEY-----

Notice that EC_ has removed. Underscore represents space that must be removed. Now you can enjoy valid signature message. Congrats!

Now you want to generate valid signature in debugger? You try to copy your private key:

-----BEGIN EC PRIVATE KEY-----
XXX
-----END EC PRIVATE KEY-----

… and bummer - no JWT is generated. You need to convert your private key. You can do that with:

openssl pkcs8 -topk8 -nocrypt -in ec1.pem -out ec2.pem

Paste your private key in ec1.pem, run that command to generate ec2.pem. Use generated ec2.pem in debugger.

In every step where public/private key is used I have replaced \n with actual newlines .

Enjoy now you can validate and/or generate jwt in debugger.

1 Like

Ah, yes. jwt.io Debugger needs the private key in pk8. Thanks for that!!

Ok, I did as you said and I can now verify my key in jwt.io Debugger.

BUT… if I take the generated token from jwt.io and add it here:

curl -H "Authorization: Bearer JWT_TOKEN" 'https://api.coinbase.com/api/v3/brokerage/accounts'

I still get an Unauthorized error.

I made sure my key is enabled in coinbase cloud. I made sure I have the Trade/View permissions. So I’m wondering if my header or payload is incorrect.

Do these look ok to you?
Header

{
  "alg": "ES256",
  "kid": "organizations/cbafb4b3-3756-48fd-adb6-d3ac5c395fd7/apiKeys/b048fb55-8916-4f57-a106-16b323c8e3e7",
  "nonce": "7c3fa2de40395a72",
  "typ": "JWT"
}

Payload
(obviously the timestamps will be expired now, but I made sure to run my curl command within the timeframe)

{
  "aud": "retail_rest_api_proxy",
  "exp": 1709042115,
  "iat": 1709041995,
  "iss": "coinbase_cloud",
  "nbf": 1709041995,
  "sub": "organizations/cbafb4b3-3756-48fd-adb6-d3ac5c395fd7/apiKeys/b048fb55-8916-4f57-a106-16b323c8e3e7",
  "uri": "GET api.coinbase.com/api/v3/brokerage/accounts"
}

Specifically, I don’t know if the aud section is correct. I copied that from the coinbase example… but I don’t know what it means.

Your underscore might be one problem? Try coinbase-cloud

JWT_TOKEN here means that you literally replace with your token? Or do you use environment variable? If so you are missing $ before it.

In their examples aud is array. So maybe try to change it to "aud": ["retail_rest_api_proxy"]?

Updated header

{
  "alg": "ES256",
  "kid": "organizations/cbafb4b3-3756-48fd-adb6-d3ac5c395fd7/apiKeys/b048fb55-8916-4f57-a106-16b323c8e3e7",
  "typ": "JWT"
}

Updated payload:

{
  "aud": ["retail_rest_api_proxy"],
  "exp": 1709047949,
  "iat": 1709047829,
  "iss": "coinbase-cloud",
  "sub": "organizations/cbafb4b3-3756-48fd-adb6-d3ac5c395fd7/apiKeys/b048fb55-8916-4f57-a106-16b323c8e3e7",
  "uri": "GET api.coinbase.com/api/v3/brokerage/accounts"
}

Still Unauthorized. I’m about ready to pull my hair out…

JWT_TOKEN here means that you literally replace with your token? Or do you use environment variable? If so you are missing $ before it.

I’m running the curl command from the commandline. I’m simply replacing the JWT_TOKEN with the one generated from jwt.io.

Ok, I’m going to go create yet another set of keys… and try again.

Updated header does not have nonce.

Bah! I deleted it, testing to see if it was needed or not. Just readded it, still no dice:

{
  "alg": "ES256",
  "kid": "organizations/cbafb4b3-3756-48fd-adb6-d3ac5c395fd7/apiKeys/b048fb55-8916-4f57-a106-16b323c8e3e7",
  "nonce": "d51369067138880f",
  "typ": "JWT"
}

You need to update also payload:
iatnbf

Yep, I deleted that too, since the examples didn’t have it. I re-added it. Tried it once replacing iat with nbf and once with both iat and nbf. Still no dice.

From valid token:

{
  "typ": "JWT",
  "alg": "ES256",
  "kid": "organizations/my-org-id/apiKeys/my-key-id",
  "nonce": "1709033554"
}
{
  "uri": "GET api.coinbase.com/api/v3/brokerage/accounts",
  "sub": "organizations/my-org-id/apiKeys/my-key-id",
  "iss": "coinbase-cloud",
  "aud": [
    "retail_rest_api_proxy"
  ],
  "nbf": 1709033554,
  "exp": 1709033614
}
1 Like

Removing nbf from payload causes Unauthorized error. Adding iat as extra field did not case any problems.

So if you have all correct/required fields, perhaps you did not update timestamps? Sadly it does not say that token has expired, it will still just return Unauthorized error.

Wait, its working… My nonce. I was creating a random 16 character hex string… I changed it to be the time (as in your example) and now its working! Finally. Thank you so much!

Coinbase examples uses hex string, and SDK also has "nonce": secrets.token_hex(). So I doubt it was a problem. I would guess it was expired / too old timestamp…

Yep, you’re correct. I just reverted back to my old nonce and it still worked. As you said, I think it was the inclusion of the nbf. I must have messed something else up when I tested adding it earlier. Removing it now, makes my auth fail. Either way, I’m good now. I truly appreciate your patience and help!

1 Like