Integrating with Atlassian Connect
Pre-note: I am going to assume that you have read through my “Writing a static Atlassian Connect add-on” guide before trying to read through this more advanced version. In this guide, we are going to explain how to integrate any arbitrary third party web framework with Atlassian Connect. We will be writing a dynamic add-on instead of a static add-on because most web frameworks involve writing server side code.
When would I need a dynamic add-on
Writing a static add-on in Atlassian Connect is a breeze; However, sometimes there are simple tasks that you will want to do in an add-on that is not possible with static add-ons. Some examples of tasks that are either difficult or impossible to accomplish with a static add-on include:
- Listening to webhooks
With a static add-on, there is no service in the background to listen and react to webhook events.
- Integrating with all third party tools
With a static add-on, you may be able to make REST calls to an external service that has a CORS enabled API. However, there are many services out there on the web that you will want to integrate with that don’t provide such rest API’s.
- Processing data
Many add-ons will want to take data from the host product, operate on that data, and then return it to the user in a much more useable manner. Atlassian Connect add-ons that provide reports are an excellent example of this. For example, instead of calculating a result from scratch in the user’s browser often your add-on can be more performant by saving customer data in your own data structures and performing queries on that instead.
These are just some examples of when a dynamic add-on will begin to be the better option, and you will likely find more reasons when you develop your own Atlassian Connect add-ons.
With that in mind: how do we write dynamic Atlassian Connect add-ons in our language of choice? And how do I extend an existing application to integrate with Atlassian Connect? The answer to those questions is in the following sections.
Which languages / frameworks can I use to write a dynamic Atlassian Connect add-on?
Short answer: any language at all so long as your application responds correctly to the Atlassian Connect interface. Any web service can behave like an Atlassian Connect add-on.
For this post I have written a template Atlassian Connect add-on in the Ninja java web framework: I call this template ninja-connect. I could have picked any programming language to write add-ons in. In-fact, I have written the following add-ons:
- My Reminders (Haskell)
- Hackathon (Haskell)
- Entity Property Tool (nodejs)
And the Atlassian Connect team maintains a list of other languages and frameworks that are available for use. Please choose any tool that best suits your needs when writing an Atlassian Connect add-on.
For the sake of simplicity, all examples in this post will be from the ninja-connect framework. All examples are in Java.
What do I need to build to integrate a web framework with Atlassian Connect?
The question that naturally arises is:
I want to write an Atlassian Connect add-on in <webframework>. What code do I need to write so that it integrates seamlessly?
The answer to that question is the subject matter of this post. The short answer is:
- All Atlassian Connect iframes (web panels) must load all.js from the host product.
- You are required to handle the installed lifecycle flow so that you can save tenant specific information. JWT authentication requires this information.
- You must be able to validate incoming JWT tokens to verify that iframe page load requests and webhook POSTs do come from the product that the POST says it does.
- If you wish to make requests back to the host product, then you need to be able to sign HTTP Requests with a JWT token.
If you implement a web framework such that it can handle all four of these scenarios, then you have officially integrated your web framework with Atlassian Connect. If you follow the links to the source code above you will see the ninja-connect implementations for all of those.
Writing code to accomplish the above four tasks is quite achievable, let’s look at how I did it with ninja-connect. I am not going to explain how the Ninja web framework works. Instead, I am going to focus on the parts that are integral to hooking up your framework with Atlassian Connect.
Creating a Tenant model in your Atlassian Connect add-on
Atlassian has thousands of customers, and your add-on must be able to handle being installed in any (or all) of those customers products.
For example, 1000 JIRA customers could choose to install your add-on, you need to have a way to separate the data for each customer from each other cleanly; it will be a privacy and security issue if one customer is ever able to see another customer’s data.
We consider each unique “host product” (Jira / Confluence) that installs your add-on to be a “tenant” of your add-on.
Thus, when a customer installs your add-on into an Atlassian Cloud product, then you will receive an “installed” lifecycle event that will contain a set of essential information that you will need to save and be able to recall for future requests. The data structure that you use to store this information is your Tenant model.
In the ninja-connect framework, we use JPA combined with the PostgreSQL relational database to create a Tenant and store the data for that tenant in a database. Please have a quick read through the ninja-connect Tenant model definition.
The fields that you will need to store against your Tenant model (at a minimum) are those that are returned to you in the installed lifecycle event: the documentation describes those fields in “Lifecycle payload”.
You can create your Tenant model in any way that you wish but you must have a tenant model and you must be able to persist the data provided by your tenants. In order to be able to authenticate requests in the future you will need this information.
Handling the installed lifecycle event
We mentioned the installed lifecycle event in the previous section but we did not explain how to handle that event. Lets break down the lifecycle event in more detail and explain the steps in the flow:
- The customer installs your Atlassian Connect add-on.
(Important: your add-on must declare that it wants JWT authentication)
- The host product POSTs a callback to the url that you have declared in your lifecycle.installed section of your descriptor. The payload of that POST contains data that you need to save.
- Your add-on inspects the data that was POSTed to you and decides what to do with it. If it was a valid installation then you will save the data and send back a HTTP 204 No Content response. If it was an invalid installation then you will not save or update your persistence stores and will also return a HTTP error.
But how do you know when the installation was invalid? Perhaps that is not a good question. A better question might be:
How could a third party attacker exploit this system to gain access to data that they should not get access to?
We want to block an attacker from getting access to customer data, thus any installation attempt that could take an existing customers tenant and turn it into the attackers tenant should result in an error.
There are two important pieces of information in the lifecycle response that you need to pay attention to in your lifecycle handler to prevent an attacker from getting data that does not belong to them:
Have I ever seen this clientKey before? Every tenant should generate its own unique clientKey.
Have I ever seen this baseUrl before and is a tenant with a different clientKey using it already?
Now lets inspect all of the different scenarios for these clientKeys and baseUrls:
As you can see this means that the only valid state for you to accept an installation request and persist it into your database is when you have never even seen the provided client key and base url before.
However, you can ascertain that an upgrade or re-installation of your add-on has occurred by ensuring that the client key and the base url are still the same as they were originally. This is useful for recognising when tenants were uninstalled and have reinstalled your add-on again.
Verifying incoming JWT tokens
Now that you have a Tenant model and you can accept new installations you need to have the ability to verify the requests that the host product is making to your add-on. This means JWT verification.
This process, generally speaking, involves the following steps:
- Reading the JWT token from the incoming HTTP request
It may exist in query parameters with the key jwt or the Authorization header with the prefix “JWT ”.
- Read the client key out of the token.
The token issuer should match the client key of the host product that make the the request. You should be able to extract it directly. You do not need to do any validation on this step.
- Get the shared secret for that tenant
Using the client key as an identifier look up the matching tenant in your persistence storage. This is so that you can get the shared secret for validation purposes. Hold on to the tenant.
- Using the shared secret, validate the original JWT token
Now that you have the shared secret you can validate if the original token was legitimate. Note that this means that you need to be able to parse the JWT token and validate in separate steps. Make sure that your JWT library of choice can do that; most should be able to.
- Store the tenant in the context of the request for future use.
That way you don’t have to constantly look up the tenant for the remainder of your request handling code.
And that is all that you need to do. If you encounter an error at any point in the time then you can fire back the appropriate HTTP error code.
There is an example of the JwtFilter used in the following section. You simply attach it to the correct controller routing method in your Ninja application and all of the hard work is done for you. Very convenient.
Writing a base Atlassian Connect iframe panel
Loading all.js is necessary to use the AP object and access Connect APIs.
Since you have to load this resource from the host product you need to know the address (base url) of the host product. The url structure that you will be eventually be requesting will be something like:
You might remember, from the previous sections, that your Tenant model contains the baseUrl for that tenant and it is set when the installed lifecycle event is handled by your add-on.
If you have verified the request for that particular iframe then you can load the correct Tenant model and pass the base url straight to the view to be rendered. This makes it very easy for dynamic add-ons to be loaded. So, to see how it is implemented in the ninja-connect framework lets go through the code step by step. Read the comments in this snippet:
This example is from the ninja-connect source code.
From the example above we know that the productBaseUrl variable will be injected into the freemarker template. From there it is as simple as writing the following code in our template file:
You can see this code in the connect-panel.ftl.html file in the ninja-connect framework. This is all that is required to make sure that you can load your HTML content in an iframe.
However, you may have noticed that connect-panel.ftl.html contains more than just code to load all.js. It also includes:
- All of the Atlassian User Interface (AUI) resources
AUI is an amazingly easy way to match the Atlassian UI look and feel inside your application. It is also super easy to use with great documentation. It is highly recommended that you style your UI elements with AUI so that your add-on feels like it belongs inside the Atlassian application that you are injecting yourself into. Check it out now.
- The ac-content div wrapper if you want automatic resizing
If you want the Atlassian Connect framework to automatically resize your iframe when your content changes then you should wrap your content in a div that has the class ac-content. This is not strictly required, but if you don’t use it then you will need to use AP.resize.
And with that you now have a basic template for every Atlassian Connect web panel that you can use for all future Atlassian Connect applications. Please feel free to add any extra HTML, CSS, Fonts or JS that you need.
Signing outgoing HTTP Requests with JWT tokens
At this point in time we can save tenants, verify incoming requests and create iframes that will be rendered inside an Atlassian host product. The missing part of this picture is our add-on server making requests to the host product. To solve that we need to prove to the host product that we are the add-on that was installed into it; we need an authentication mechanism.
We can use JWT as our authentication mechanism to talk to the host product; all we have to do is add a valid JWT token to the HTTP request that we make back to the Alassian host product.
This process is explained thoroughly in the Atlassian Connect documentation but we can surmise some key points:
- The Query String hash (qsh) is the hardest part
The hardest part of generating the JWT token is providing the only part of the token that is not part of the vanilla JWT specification: the query string hash. The query string hash is used by Atlassian products to ensure that you can only make a HTTP request to the exact URL that the JWT token was generated to access. This means that an attacker can’t get hold of the JWT token and make any request that they please; instead they must make exactly the request that the token was created to authenticate. Generating this token correctly is tricky but the Atlassian documentation has an excellent guide on how to write your own generator. There are also existing implementations that you can compare your implementation against.
- The JWT issuer (iss) should be your add-on key
At the top level of your descriptor you would have provided an add-on key. This is the key that must be used as the issuer (iss) when generating the JWT token.
- The shared secret for that host must be used
It may sound obvious but the shared secret for the host that you are trying to communicate with must be the shared secret that you use to sign the JWT token. Lucky for you you have a Tenant model so it should be easy to look up the shared secret for any given host product.
With those points in mind you can look at the RequestAuthenticator in the ninja-connect codebase. In essence it is just:
- Setting the issuer to be the plugin key.
- Generating and setting the query string hash for the JWT token.
- Putting the JWT token on the provided request.
With this method you can turn any apache http-client request into a JWT authenticated request. This is extremely convenient.
Congratulations! At this point in time you have successfully integrated your web framework with Atlassian Connect.
Where to from here?
Getting through this entire guide was a big achievement: well done! From here you can see Ninja Connect bring all of this together in a single webhook event callback. In this webhook issue create event callback we can see:
- Verification of the JWT token on the incoming request
- Use of our Tenant model
- Signing of the outgoing HTTP request back to the host product
You can now play around with the ninja-connect framework to get a better feel for how easy it is to integrate Atlassian Connect into any web framework. I think that it is important to note that full integration with Atlassian Connect for the Ninja framework took a mere three files: one template and two java helper classes. That is a pretty low barrier to entry via integration. I am confident that you can integrate any language that has solid JWT support.
I can’t wait to see what you build next and I hope that this guide helps you build great Atlassian Connect integrations. If you build one then don’t hesitate to let me know about it! I would love to see it.