Setting Up a JWT Enabled Connect Add-On Using Ruby on Rails

In March we had the great opportunity to get acquainted with the newest technologies and frameworks provided by Atlassian for their add-on developers. As a new vendor, MeisterLabs was invited to Amsterdam for Connect Week in order to get started with our first add-ons and to help shape Atlassian’s EAP features for their Connect frameworks. It only took us one more month to release our first add-on, with the next ones soon to follow.

Setting Up a JWT Enabled Connect Add-On Using Ruby on Rails

Our products are built on a Ruby on Rails backend, so we never considered using any other language for the add-ons. This was quite unique for the Atlassian environment, as we would soon discover, because very few other vendors use the same stack as we do, most of them relying on Node, Java, or Python.

The following tutorial will demonstrate how to setup a Confluence Connect add-on on top of an existing RoR application and assumes you have basic RoR knowledge. Even though the main focus is Confluence, most of the concepts and code shown here work for JIRA and Bitbucket as well.

1. Getting Started

1.1 Get a development version of Confluence

Register for a new development instance at in order to gain access to Confluence and JIRA. Bitbucket offers sandbox capabilities inside their live system. For more details on how to set this up and how to enable development mode in your new Confluence instance, please follow the instructions from the official documentation.

Get started with Atlassian Cloud

1.2 Ngrok

In order for your add-on to be accessible by the Confluence test instance you will need to provide a publicly accessible URL to your Ruby on Rails application. Ngrok is one of the easiest ways to do this. Download it here and then simply start it with ./ngrok 3000. Copy the generated https URL generated and paste in your application’s configuration file as the site_url.


config.site_url = ''

2. Building the Add-On

The first step in creating your add-on is deciding which Atlassian modules you will want to use. A summary of the available ones can be found in the documentation. We will continue this tutorial with a dynamic content macro built on top of a Todo demo application.

2.1 Add-on descriptor

Create a new controller in your application, ConfluenceController, and then create a new file in the views/confluence folder called descriptor.json.erb with the following content:

 "name": "Todo",
 "description": "TODO Atlassian Connect add-on",
 "key": "<%= add_on_key %>",
 "baseUrl": "<%= Todo::Application.config.site_url %>",
 "vendor": {
   "name": "Cats at work inc.",
   "url": ""
 "authentication": {
   "type": "none"
 "modules": {
   "dynamicContentMacros": [
       "url": "/confluence/get_task?id={id}",
       "description": {
         "value": "Displays a TODO task inline"
       "icon": {
         "width": 80,
         "height": 80,
         "url": "/images/confluence/logo_300x300.png"
       "parameters": [
           "identifier": "id",
           "name": {<
             "value": "Task ID"<
           "description": {
             "value": "The task ID from TODO"
           "type": "string",
           "required": true
       "name": {
         "value": "TODO"
       "key": "dynamic-task-macro",
       "featured": true
 "apiVersion": 1

“key” – A unique key to identify the add-on, <= 80 characters. Since you will probably test your add-on across different environments (development, alpha, production) we recommend building your key environment dependent.

Eg. “connect-add-on-todo-#{Rails.env}”

“authentication” – We will start with no authentication, so “none”.

“baseUrl” – The base url of the remote add-on, which is used for all communications back to the add-on instance.

For displaying our tasks inline the add-on will make a request to the URL that we specify in the descriptor under “modules”.”dynamicContentMacros”.”url”. Make sure the path you provide handles fetching a task from the DB based on the passed ID parameter.

2.2 A simple view

The get_task method that fetches a task from the DB also needs to render a view that will be embedded in the users’ Confluence pages. Create the get_task.html.erb view under views/confluence with the following content:

<link rel="stylesheet" media="all" href="//">

<section id="content" class="ac-content">

<aui-badge><%= @task.title %></aui-badge>


<script id="connect-loader" data-options="sizeToParent:false;">

(function() {

  var getUrlParam = function (param) {

    var codedParam = (new RegExp(param + '=([^&]*)'))


    return decodeURIComponent(codedParam);


  var baseUrl = getUrlParam('xdm_e') + getUrlParam('cp');

  var options = document



  var script = document.createElement("script");

  script.src = baseUrl + '/atlassian-connect/all.js';

  if (options) {

    script.setAttribute('data-options', options);





“aui.min.css” – You need to include this CSS file if you want to use Atlassian’s styling guidelines and library.

“connect-loader” – In order for your add-on to be correctly loaded in the Confluence page, your view needs to include the all.js script provided by the Atlassian instance. The URL of this script is, however, relative to the current instance and therefore you need to build the URL dynamically. For further extensibility create a partial with the script and include it in all your future add-on related views.

2.3 Install and run the add-on

If you closely followed the steps above, the URL for installing your add-on is After installation, create a Confluence page and insert your new macro in the page. Depending on your application styling and the page content, it should look something like this:

Confluence page with macro

3. Adding Authentication

So far all requests coming to your application contained no authentication information, so your application couldn’t make sure that the requests were coming from a valid add-on installation instance, and you had no information about the current user accessing your add-on.

Atlassian Connect uses a technology called JWT (JSON Web Token) to authenticate add-ons. Basically a security context is exchanged when the add-on is installed, and this context is used to create and validate JWT tokens, embedded in API calls. The use of JWT tokens guarantees that:

  • The Atlassian application can verify it is talking to the add-on, and vice versa (authenticity).
  • None of the query parameters of the HTTP request, nor the path (excluding the context path), nor the HTTP method, were altered in transit (integrity).

3.1 Declare that the add-on uses JWT

Replace the authentication declaration from your descriptor:

"authentication": {

 "type": "none"



"authentication": {

 "type": "jwt"


"lifecycle": {

 "installed": "/confluence/installed",

 "uninstalled": "/confluence/uninstalled"


Add to ConfluenceController the installed and uninstalled methods that simply render(nothing: true).

When an administrator installs the add-on in an Atlassian cloud instance, Connect initiates an “installation handshake”: it invokes the endpoint, passing a security context. This security context will then be stored for future use. The security contexts contains, among other things, a key identifying the add-on and a shared secret (used to create and validate JWT tokens). The security context to validate incoming requests (e.g. Webhooks), and sign outgoing requests (e.g. REST API calls to JIRA).

3.2 Add external libraries

Add to your Gemfile:

gem 'atlassian-jwt-authentication',
require: require 'atlassian_jwt_authentication'

And then make sure to run bundle install. This gem handles all the logic necessary for authenticating your add-on, as well as signing future REST API calls. It relies on certain database tables to be present and it provides a generator for setting these up:

rails g atlassian_jwt_authentication:setup
rake db:migrate


These 2 tables will store the JWT details: add-on key, client key, shared secret, and the user key sent by the Atlassian product. More information can be found on the gem’s homepage.

For handling the installation lifecycle use the filters provided by the gem and add this to your controller:

include AtlassianJwtAuthentication
before_action :on_add_on_installed, only: [:installed]
before_action : on_add_on_uninstalled, only: [:uninstalled]


The installed and uninstalled endpoints don’t need to render anything, they just need to respond with the appropriate HTTP codes (200 for success, 401 for failed authentication – handled by the gem).

on_add_on_installed stores in the DB the add-on key, client key, shared secret, and the user key. If the add-on was previously installed it will receive an extra JWT parameter which the gem verifies using the existing shared secret.

on_add_on_uninstalled first checks the received JWT parameter and if the verification is successful destroys the related DB entries created upon add-on installation.

Once JWT authentication is enabled for your add-on all requests done to the endpoints that you declared will contain either a JWT parameter or an authentication header containing the JWT token. The gem handles the authentication for you, returning 401 if the token cannot be verified.

Add the following filter to all the methods that acts as add-on endpoints:

before_action only: [:get_task] do |controller|

controller.send(:verify_jwt, add_on_key)



def add_on_key

# return your environment dependent add-on key here



The JWT token is sent either as an Authorization header or as a parameter. installed & uninstalled endpoints, renderModes URLs receive the JWT as a header, whereas the macro view, macro editor receive the JWT as a parameter. The gem handles both cases for you.

When using the gem you have access to the Atlassian authentication information with the current_jwt_auth helper and to the current Atlassian user information with the current_jwt_user helper. You can extend these if you need to attach your own user information for authorization.

4. Ready to Go

Your authenticated Connect add-on is ready to go! From this point on it’s up to you to explore further options for your add-on. If you are a Ruby on Rails developer, we encourage you to contribute to the gem and help us make it even better.

This article was written by MindMeister’s lead developer Laura Bârlădeanu. You can learn more about Laura on CodePancake or follow her on Twitter.