Validating and generating Atlassian JWT
When developing a add-on to cloud Jira, a usual need is to communicate with the Jira using REST API. The authentication is done using JWT and it took us a while to figure out how to validate and generate them. At the end of the day, it’s not a complicated process but there are a few things one can overlook and be stuck with the implementation. We will describe the complete implementation in this blog post.
When the plugin is installed, Jira will call the endpoint that you registered in the atlassian-connect.json as an Installed callback. You will usually want to use this callback to save the shared secret, that is passed to you by Jira. The shared secret is used for signing your JWT tokens or validating JWTs generated by Jira.
It’s also important to validate the JWT from the Installed callback itself. Let’s validate the algorithm first. We will get the signing algorithm of JWT from the JWT using atlassian-jwt library first. The algorithm is then checked against supported algorithms that are hardcoded in the plugin.
Raw JWT is the raw string (for example eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.K1lVDxQYcBTPnWMTGeUa3gYAgdEhMFFv38VmOyl95bA) taken either from query parameters or from Authorization header. In the snippet below, the even object is the AWS lambda event (pretty much the request).
Validation of Installed callback is specific because it uses asymmetric JWT. It means, the Jira signed the JWT using a private key and you have to validate it using the public key. When the token is symmetric (all other callbacks except the Installed and Uninstalled), the JWT is signed and validated only using one key (shared secret).
We need to get the public key first using an id, that is named kid. It's in the JWT and we will use the Atlassian library to get it. We can then download the public key from the URL https://connect-install-keys.atlassian.com/kid using the fetch library.
In the next step, the JWT is finally decoded and signature verified using the public key. We know someone manipulated with the JWT when the signature doesn’t match. In that case, the exception is thrown.
We should also validate claims from the token. Claims are just the JSON payload of tokens. AUD should contain the URL of your plugin. It’s up to you how to get it. In our case, the URL is generated from the AWS API Gateway resource as an environmental variable in the AWS Cloudformation definition. The utility.getPluginURL just returns this variable.
Validating expiration is easy. You can use the moment library to get the current time and check if it’s after the EXP.
And the last check of Installed JWT - compare the hash of the target URL (URL to your Installed endpoint) against the QSH. We will use the Atlassian library to generate the hash. The event object is again the AWS lambda event - the request from Jira.
And that’s it. Now when we know the JWT is valid, we can save the shared secret, client id and cloud Jira base URL to the database. Keep in mind you cannot call the Jira in the Installed callback yet even if you already have the shared secret. The Jira simply won’t accept your requests because the installation of your plugin isn’t complete until you send response 200. If you need to call Jira during the installation, do it in the Enabled callback.
We will validate the token from the Uninstalled callback using the exact same process. There is only one difference - one additional claim to validate. It is the issuer - client id received in the installed callback.
You need to search the client id in your database using the base Jira URL. The Jira URL is usually in the request body under the name baseUrl or in the query parameters under the name xdm_e. I’m not putting here the code of method getClientIdByJiraBaseURL because it’s way too specific and implementation depends pretty much on the database you are using.
Validation of symmetric JWT
In all other cases (except the Installed and Uninstalled callback), the JWT from Jira has to be validated in a slightly different way. The code for algorithm and claims validation is the same, so I will skip it.
- Validate algorithm
- Search shared secret in your database using the client id or base Jira URL from the request
- Decode and validate the JWT using the code below
- Validate claims ISS, EXP and QSH
Generation of JWT
In many cases, you will be the one who is going to initiate the communication with cloud Jira. To authenticate your plugin, you have to generate the token and send it in the Authorization header. First, you need to find the shared secret of the target cloud Jira instance you want to communicate with.
The payload will contain claims ISS, IAT, EXP and QSH. If claims are created correctly, Jira should accept your request.
ISS - issuer of the JWT. When we validated the JWT, the issuer was the client id but this is now different! When you generate the JWT, this has to be the app key of your plugin (the unique ID of your plugin that’s provided in the atlassian-connect.json). It’s easy to make a mistake here.
IAT - UTC time when the JWT was created
EXP - expiration time of your token. The JWT isn’t reusable because of QSH claim, so it doesn’t make sense to make a longed lived token. 3 minutes should be enough.
QSH - hash of the target URL, query parameters, and method of your request.
And here is the last snippet that shows how to use the generated token. We will issue a request to persist a plugin property to the cloud Jira. The code above was wrapped to method generateTokenCustomSecret that takes the method, request URL, and shared secret.
Author: Luděk Novotný