Understanding JWT for Atlassian Connect

January 26th 2015 Travis Smith in JWT, Atlassian Connect

Atlassian Connect uses JSON Web Token (JWT) for authentication between the host product (e.g. JIRA, Confluence, or HipChat) and your add-on. To ensure the security of everyone's data, Atlassian includes additional claims so a signed request cannot be intercepted and used to perform other actions. We offer frameworks that hide that complexity for node.js, Play!, and ASP.NET as well as atlassian-jwt for those working on other Java stacks. What happens when you don't want to use our frameworks? That's not a problem, you can use whatever framework you want - one of the joys of building add-ons with Atlassian Connect. You can implement the JWT authentication yourself — I'll walk you through the added security features Atlassian uses with Connect.

There are two ways one might interact with JWT signed requests. You need to know how to validate a signed request made to your add-on from the host application and you need to know how to properly generate the claims and sign a request you made out to the host product (e.g. REST API calls). For this post, we're going to assume that you have a library able to do the encoding and decoding of the JWT signature. What we're focusing on is how to deal with the claims attached to that signature.

The first bit to remember is that there's a shared secret provided during the install callback that happens at the start of the Connect lifecycle. You will need this shared secret to do anything with the JWT signature. In addition, the install callback also contains a clientKey that's used to distinguish between product hosts.

Validating a request against your add-on is the simplest place to start. If you have a webhook endpoint registered in your atlassian-connect.json descriptor, then it's possible for you to receive messages from the host product. Validating the source of the request makes sure a malicious actor can't start sending requests and corrupting or exposing data they shouldn't have access to. The first thing to do is decode the JWT signature using the shared secret key provided during the install callback. You should end up with a JSON representation of the claims provided in the signature. Something similar to

{
    "iss": "jira:15489595",
    "iat": 1386898951,
    "qsh": "8063ff4ca1e41df7bc90c8ab6d0f6207d491cf6dad7c66ea797b4614b71922e9",
    "exp": 1386899131
}
iss
The issuer of the claims. In the case of validating a request against your add-on, this should be the clientKey.
iat
The "issued at" time. This number should be number of seconds since UNIX epoch.
exp
the time this signature should expire. Normally it's a short period of time after iat. exp is in the same format at iat.
qsh
A custom claim to the Atlassian Connect world, and it describes the resource and query string this request is for. This ensures that someone cannot sniff out a token and reuse the token to gain access to other resources.

To generate the query hash, or qsh, we need HTTP method, resource URI, and query string for the request. An example would be POST https://app.my-add-on.com/hooks/issue_updated. To convert this into a query hash, we need first the HTTP method, in this case POST. Then we combine that with the resource URI, relative to the base of your add-on (or the context path of the product). In the example it would be /hooks/issue_updated. Then we need the query string in canonical format. The canonical format is basically escaping then sorting all the query string parameters so that the generated value will always match for a given resource. More examples of that later, but first let's wrap all these parts together. We have the {HTTP method}&{resource URI}&{canonical query string}, which in this case is simply POST&/hooks/issue_updated&. If for some reason the resource URI would be blank, replace it with /. This makes sure requests for http://localhost are no different than requests for http://localhost/. Once you have the string formed, hash it with SHA-256 and convert those bytes to a string using hex representation. And there's your query hash claim.

Now, when sending a request to an API, it might be more complicated. Signing of your requests to the host product has some slight differences. iss is your add-on key instead of the clientKey. If your requests have query string parameters, like say https://jira.atlassian.com/rest/api/2/search?startAt=2&maxResults=4&fields=summary,comment&expand=names does, then we need to walk through generating the canonical query string part of the query hash. The first part, the HTTP method, is simple and is GET in this case. The URI is /rest/api/2/search. The parts of the query string need broken up as well, so you have

startAt=2
maxResults=4
fields=summary,comment
expand=names

Given that list, we need it sorted and values percent escaped. If you have multiple parameters with the same name, the values are sorted then joined together with a comma.

expand=names
fields=summary%2Ccomment
maxResults=4
startAt=2

You'll see fields went from summary,comment to summary%2Ccomment. It's important the percent encoded characters are uppercase. Now take the results and join it all together with the other members like

GET&/rest/api/2/search&expand=names&fields=summary%2Ccomment&maxResults=4&startAt=2

Hash that, stick it in qsh and send your claims off to your JWT encoder with the secret key for the client in question. Add a Authentication: JWT $token header to your request with the result of the JWT encoder and you have a signed request.

A few items to keep in mind that might help out...

  • percent encoded characters in query string hashes should be like %2A, not %2a (capital A-F)
  • sort both the query string parameters & query string values for comma separated lists
  • format of the qsh claim hash is lowercase a-f
  • method name should be in all caps (e.g. GET, PUT, DELETE, not Get or get)
  • if the URI portion would be blank, put a / in
  • remember to ignore JWT parameter when creating the query hash claim
  • the signature should use the HMAC SHA-256 (HS256) algorithm, check with your JWT library for configuration options
  • use the JWT decoder if you're having troubles with a signed request

It's not that difficult to build your own JWT authentication — many add-ons do this now and so can you! Do you have an internal system that would benefit from integrating with your Atlassian tools, or a great add-on idea? Get started with Atlassian Connect today.