Atlassian JWT Authentication provides support for handling JWT authentication as required by Atlassian when building add-ons: https://developer.atlassian.com/static/connect/docs/latest/concepts/authentication.html
You can check out the latest source from git:
git clone https://github.com/MeisterLabs/atlassian-jwt-authentication.git
Or, if you're using Bundler, just add the following to your Gemfile:
gem 'atlassian-jwt-authentication',
git: 'https://github.com/MeisterLabs/atlassian-jwt-authentication.git'
This gem relies on the jwt_tokens
table being present in your database and
the associated JwtToken model.
To create those simply use the provided generators:
bundle exec rails g atlassian_jwt_authentication:setup
If you are using another database for the JWT data storage than the default one, pass the name of the DB config to the generator:
bundle exec rails g atlassian_jwt_authentication:setup shared
Don't forget to run your migrations now!
The gem provides 2 endpoints for an Atlassian add-on lifecycle, installed and uninstalled. For more information on the available Atlassian lifecycle callbacks visit https://developer.atlassian.com/static/connect/docs/latest/modules/lifecycle.html.
If your add-on baseUrl is not your application root URL then include the following configuration for the context path. This is needed in the query hash string validation step of verifying the JWT:
# In the add-on descriptor:
# "baseUrl": "https://www.example.com/atlassian/confluence",
AtlassianJwtAuthentication.context_path = '/atlassian/confluence'
The gem will take care of setting up the necessary JWT tokens upon add-on installation and to delete the appropriate tokens upon un-installation. To use this functionality, simply call
include AtlassianJwtAuthentication
before_action :on_add_on_installed, only: [:installed]
before_action :on_add_on_uninstalled, only: [:uninstalled]
Furthermore, protect the methods that will be JWT aware by using the gem's JWT token verification filter. You need to pass your add-on descriptor so that the appropriate JWT shared secret can be identified:
include AtlassianJwtAuthentication
# will respond with head(:unauthorized) if verification fails
before_filter only: [:display, :editor] do |controller|
controller.send(:verify_jwt, 'your-add-on-key')
end
Methods that are protected by the verify_jwt
filter also give access to information
about the current JWT token instance and logged in account (when available):
current_jwt_token
returnsJwtToken
current_account_id
returnsString
Furthermore, this information is stored in the session so you will have access to these 2 instances also on subsequent requests even if they are not JWT signed.
# current_jwt_token returns an instance of JwtToken, so you have access to the fields described above
pp current_jwt_token.addon_key
pp current_jwt_token.base_url
If you need detailed user information you need to obtain it from the instance and process it respecting GDPR.
The initial call to load the iframe content is secured by JWT. However, the loaded content cannot sign subsequent requests. A typical example is content that makes AJAX calls back to the add-on. Cookie sessions cannot be used, as many browsers block third-party cookies by default. AJA provides middleware that works without cookies and helps making secure requests from the iframe.
Standard JWT tokens are used to authenticate requests from the iframe back to the add-on service. A route can be secured using the following code:
include AtlassianJwtAuthentication
before_filter only: [:protected] do |controller|
controller.send(:verify_jwt, 'your-add-on-key', skip_qsh_verification: true)
end
In order to secure your route, the token must be part of the HTTP request back to the add-on service. This can be done
by using the standard jwt
query parameter:
<a href="/protected?jwt={{token}}">See more</a>
The second option is to use the Authorization HTTP header, e.g. for AJAX requests:
beforeSend: function(request) {
request.setRequestHeader("Authorization", "JWT {{token}}");
}
You can embed the token anywhere in your iframe content using the token
content variable. For example, you can embed
it in a meta tag, from where it can later be read by a script:
<meta name="token" content="{{token}}">
#### Add-on licensing
If your add-on has a licensing model you can use the `ensure_license` filter to check for a valid license.
As with the `verify_jwt` filter, this simply responds with an unauthorized header if there is no valid license
for the installation.
```ruby
before_filter :ensure_license
If your add-on was for free and you're just adding licensing now, you can specify the version at which you started charging, ie. the minimum version of the add-on for which you require a valid license. Simply include the code below with your version string in the controller that includes the other add-on code.
def min_licensing_version
Gem::Version.new('1.0.0')
end
You can use a middleware to verify JWT tokens (for example in Rails application.rb
):
config.middleware.insert_after ActionDispatch::Session::CookieStore, AtlassianJwtAuthentication::Middleware::VerifyJwtToken, 'your_addon_key'
Token will be taken from params or Authorization
header, if it's verified successfully request will have following headers set:
- atlassian_jwt_authorization.jwt_token
JwtToken
instance - atlassian_jwt_authorization.account_id
String
instance - atlassian_jwt_authorization.context
Hash
instance
Middleware will not block requests with invalid or missing JWT tokens, you need to use another layer for that.
Build the URL required to make a service call with the rest_api_url
helper or
make a service call with the rest_api_call
helper that will handle the request for you.
Both require the method and the endpoint that you need to access:
# Get available project types
url = rest_api_url(:get, '/rest/api/2/project/type')
response = Faraday.get(url)
# Create an issue
data = {
fields: {
project: {
'id': 10100
},
summary: 'This is an issue summary',
issuetype: {
id: 10200
}
}
}
response = rest_api_call(:post, '/rest/api/2/issue', data)
pp response.success?
To make requests on user's behalf add act_as_user
in scopes required by your app.
Later you can obtain OAuth bearer token from Atlassian.
Do that using AtlassianJwtAuthentication::UserBearerToken.user_bearer_token(account_id, scopes)
If you want to debug the JWT verification define a logger in the controller where you're including AtlassianJwtAuthentication
:
def logger
Logger.new("#{Rails.root}/log/atlassian_jwt.log")
end
If you want to render your own pages when the add-on throws one of the following errors:
- forbidden
- unauthorized
- payment_required
overwrite the following methods in your controller:
def render_forbidden
# do your own handling there
# render your own template
render(template: '...', layout: '...')
end
# the same for render_payment_required and render_unauthorized
You can use rake tasks to simplify plugin installation:
bin/rails atlassian:install[prefix,email,api_token,https://external.address.to/descriptor]
Where prefix
is your instance name before .atlassian.net
. You an get an API token from Manage your account page.
Config | Environment variable | Description | Default |
---|---|---|---|
AtlassianJwtAuthentication.context_path |
none | server path your app is running at | '' |
AtlassianJwtAuthentication.verify_jwt_expiration |
JWT_VERIFY_EXPIRATION |
when false allow expired tokens, speeds up development, especially combined with webpack hot module reloading |
true |
AtlassianJwtAuthentication.log_requests |
AJA_LOG_REQUESTS |
when true outgoing HTTP requests will be logged |
false |
AtlassianJwtAuthentication.debug_requests |
AJA_DEBUG_REQUESTS |
when true HTTP requests will include body content, implicitly turns on log_requests |
false |
AtlassianJwtAuthentication.signed_install |
AJA_SIGNED_INSTALL |
Installation lifecycle security improvements. Migration process described here. In the descriptor set "apiMigrations":{"signed-install":AtlassianJwtAuthentication.signed_install} |
false |
Parameter | Description / |
---|---|
force_asymmetric_verify |
A proc expected to return 'true' if the currently processed request must be validated with RS256 algorithm. Used for signed_install endpoints. |
Ruby 2.0+, ActiveRecord 4.1+
With middleware enabled you can use following configuration to limit access to message bus per user / instance:
MessageBus.user_id_lookup do |env|
env.try(:[], 'atlassian_jwt_authentication.account_id')
end
MessageBus.site_id_lookup do |env|
env.try(:[], 'atlassian_jwt_authentication.jwt_token').try(:id)
end
Then use MessageBus.publish('/test', 'message', site_id: X, user_ids: [Y])
to publish message only for a user.
Requires message_bus patch available at https://github.com/HeroCoders/message_bus/commit/cd7c752fe85a17f7e54aa950a94d7c6378a55ed1
Removed current_jwt_user
, JwtUser
, update your code to use current_account_id
current_jwt_auth
has been renamed to current_jwt_token
to match model name. Either mass rename or add alias
in your controller:
alias_method :current_jwt_auth, :current_jwt_token
helper_method :current_jwt_auth