No `kid` in Atlassian Connect install lifecycle request

I’m developing an integration with Jira Cloud but I am struggling to verify the webhook that comes in when a user installs my app (currently just myself in a developer mode instance).

The documentation states that the public key to validate the incoming requests’ JWT token can be retrieved from the public key CDN (https://connect-install-keys.atlassian.com/).
However, I’m running into two problems with this:

  • The very first time the app is installed in a new instance, no JWT token is provided so obviously I am not able to validate it. I only receive the expected object with instance information as the POST payload (with clientKey, publicKey, sharedSecret etc.). I have tried all of these params in the payload to see if I could receive the public key with them from the CDN but I get an error stating that the key is not found every time. See screenshot of the headers below (from ngrok)

  • If I uninstall the app in the new instance and re-install it to re-test my resolver for the install lifecycle request, I do receive a JWT token. However, I am still not able to verify it. The token that I am receiving is signed using the HS256 algorithm instead of RS256 as it is supposed to as listed in the docs. This token is signed with the sharedSecret that I receive in the payload so I can verify it from that perspective, but of course, I still don’t know if the origin is genuine as this shared key is simply included in the same request.

I’ve received the following JWT token in the Authorization header (temporary instance and test app so I don’t care about the clientKey being shared):

JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2MGYwMGFkOGY3NDljNDAwNjgwZGFmNDciLCJxc2giOiJmOWI0ZGM0YzE3NzEyMzJmMzhlMGMzMjQzNTNkNTJmYjQ4NzJiNTg3N2QxNDNkNGZhNTI3NmFiNTI0MjExZjVjIiwiaXNzIjoiNTc0ZjhiZGQtOGRjNC0zNWQzLTk1NTgtN2JjZWE0ZTQxZGU2IiwiY29udGV4dCI6e30sImV4cCI6MTYzODI3MTUzMywiaWF0IjoxNjM4MjcwNjMzfQ.R4D8ntSucrhHGJFamhcgeF1JdQUCN7J94CsBMcrd6cQ

This token decodes to (jwt-cli output):

➜  ~ jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2MGYwMGFkOGY3NDljNDAwNjgwZGFmNDciLCJxc2giOiJmOWI0ZGM0YzE3NzEyMzJmMzhlMGMzMjQzNTNkNTJmYjQ4NzJiNTg3N2QxNDNkNGZhNTI3NmFiNTI0MjExZjVjIiwiaXNzIjoiNTc0ZjhiZGQtOGRjNC0zNWQzLTk1NTgtN2JjZWE0ZTQxZGU2IiwiY29udGV4dCI6e30sImV4cCI6MTYzODI3MTUzMywiaWF0IjoxNjM4MjcwNjMzfQ.R4D8ntSucrhHGJFamhcgeF1JdQUCN7J94CsBMcrd6cQ

To verify on jwt.io:

https://jwt.io/#id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2MGYwMGFkOGY3NDljNDAwNjgwZGFmNDciLCJxc2giOiJmOWI0ZGM0YzE3NzEyMzJmMzhlMGMzMjQzNTNkNTJmYjQ4NzJiNTg3N2QxNDNkNGZhNTI3NmFiNTI0MjExZjVjIiwiaXNzIjoiNTc0ZjhiZGQtOGRjNC0zNWQzLTk1NTgtN2JjZWE0ZTQxZGU2IiwiY29udGV4dCI6e30sImV4cCI6MTYzODI3MTUzMywiaWF0IjoxNjM4MjcwNjMzfQ.R4D8ntSucrhHGJFamhcgeF1JdQUCN7J94CsBMcrd6cQ

✻ Header
{
  "typ": "JWT",
  "alg": "HS256"
}

✻ Payload
{
  "sub": "60f00ad8f749c400680daf47",
  "qsh": "f9b4dc4c1771232f38e0c324353d52fb4872b5877d143d4fa5276ab524211f5c",
  "iss": "574f8bdd-8dc4-35d3-9558-7bcea4e41de6",
  "context": {},
  "exp": 1638271533,
  "iat": 1638270633
}
   Issued At: 1638270633 11/30/2021, 12:10:33 PM
   Expiration Time: 1638271533 11/30/2021, 12:25:33 PM

✻ Signature R4D8ntSucrhHGJFamhcgeF1JdQUCN7J94CsBMcrd6cQ

Since this is a HS256 token, it does not support public/private key signing and the token does not include a kid to retrieve the public key.

Am I doing anything wrong or overlooking something? It seems to me that the requests that I am receiving do not comply with the documentation.

Pff I finally solved it. It turned out that I had to put:

  "apiMigrations": {
    "signed-install": true
  },

In the atlassian-connect.json file!

To be honest, I think this should be clearly defined in the documentation for this. I only found out about this after many hours and eventually found it because of a reference to the concept of API migrations in some Atlassian source code on Bitbucket. If I had not stumbled upon it by accident I could have spent many more hours looking for it.

Now, if you simply follow the documentation you will always run into this issue. There is no mention of this API migration at all in the docs. Even the provided examples do not include it so are technically wrong.

Please put it in the docs!!!