atlassian-jwt-authentication 0.9.0.pre8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4b613184861f6210fe60369212d95288f3bc85a71a9906714c71d966e1606381
4
+ data.tar.gz: 320921baeac4b8bdfba96131fcb0a05077242f41e96b7ee3d9e3b03ae5dace01
5
+ SHA512:
6
+ metadata.gz: 026ca8a88864fdfa13db84a94d403291ee10935bdcc99a31366a5f93d467479f21c308789f3e06086d206ab4e9656e992431de705095baefbf347d954549717a
7
+ data.tar.gz: a91c87fe92112b9bef084198e2d2b6aecf82f64e3c881de0448dab04cb463be430993e8a91ead79656f42f97350bdb620778eb8d73c299027bec88f48b7a25e7
data/CHANGELOG ADDED
File without changes
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2016-2016 MeisterLabs
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # Atlassian JWT Authentication
2
+
3
+ Atlassian JWT Authentication provides support for handling JWT authentication as required by
4
+ Atlassian when building add-ons: https://developer.atlassian.com/static/connect/docs/latest/concepts/authentication.html
5
+
6
+ ## Installation
7
+
8
+ ### From Git
9
+
10
+ You can check out the latest source from git:
11
+
12
+ `git clone https://github.com/MeisterLabs/atlassian-jwt-authentication.git`
13
+
14
+ Or, if you're using Bundler, just add the following to your Gemfile:
15
+
16
+ ```ruby
17
+ gem 'atlassian-jwt-authentication',
18
+ git: 'https://github.com/MeisterLabs/atlassian-jwt-authentication.git'
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### Setup
24
+
25
+ This gem relies on the `jwt_tokens` table being present in your database and
26
+ the associated JwtToken model.
27
+
28
+ To create those simply use the provided generators:
29
+
30
+ ```
31
+ bundle exec rails g atlassian_jwt_authentication:setup
32
+ ```
33
+
34
+ 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:
35
+ ```
36
+ bundle exec rails g atlassian_jwt_authentication:setup shared
37
+ ```
38
+
39
+ Don't forget to run your migrations now!
40
+
41
+ ### Controller filters
42
+
43
+ The gem provides 2 endpoints for an Atlassian add-on lifecycle, installed and uninstalled.
44
+ For more information on the available Atlassian lifecycle callbacks visit
45
+ https://developer.atlassian.com/static/connect/docs/latest/modules/lifecycle.html.
46
+
47
+ If your add-on baseUrl is not your application root URL then include the following
48
+ configuration for the context path. This is needed in the query hash string validation
49
+ step of verifying the JWT:
50
+ ```ruby
51
+ # In the add-on descriptor:
52
+ # "baseUrl": "https://www.example.com/atlassian/confluence",
53
+
54
+ AtlassianJwtAuthentication.context_path = '/atlassian/confluence'
55
+ ```
56
+
57
+ #### Add-on installation
58
+ The gem will take care of setting up the necessary JWT tokens upon add-on installation and to
59
+ delete the appropriate tokens upon un-installation. To use this functionality, simply call
60
+
61
+ ```ruby
62
+ include AtlassianJwtAuthentication
63
+
64
+ before_action :on_add_on_installed, only: [:installed]
65
+ before_action :on_add_on_uninstalled, only: [:uninstalled]
66
+ ```
67
+
68
+ #### Add-on authentication
69
+ Furthermore, protect the methods that will be JWT aware by using the gem's
70
+ JWT token verification filter. You need to pass your add-on descriptor so that
71
+ the appropriate JWT shared secret can be identified:
72
+
73
+ ```ruby
74
+ include AtlassianJwtAuthentication
75
+
76
+ # will respond with head(:unauthorized) if verification fails
77
+ before_filter only: [:display, :editor] do |controller|
78
+ controller.send(:verify_jwt, 'your-add-on-key')
79
+ end
80
+ ```
81
+
82
+ Methods that are protected by the `verify_jwt` filter also give access to information
83
+ about the current JWT token instance and logged in account (when available):
84
+
85
+ * `current_jwt_token` returns `JwtToken`
86
+ * `current_account_id` returns `String`
87
+
88
+ Furthermore, this information is stored in the session so you will have access
89
+ to these 2 instances also on subsequent requests even if they are not JWT signed.
90
+
91
+ ```ruby
92
+ # current_jwt_token returns an instance of JwtToken, so you have access to the fields described above
93
+ pp current_jwt_token.addon_key
94
+ pp current_jwt_token.base_url
95
+ ```
96
+
97
+ If you need detailed user information you need to obtain it from the instance and process it respecting GDPR.
98
+
99
+ #### How to send a signed HTTP request from the iframe back to the add-on service
100
+
101
+ The initial call to load the iframe content is secured by JWT. However, the loaded content cannot
102
+ sign subsequent requests. A typical example is content that makes AJAX calls back to the add-on. Cookie sessions cannot
103
+ be used, as many browsers block third-party cookies by default. AJA provides middleware that
104
+ works without cookies and helps making secure requests from the iframe.
105
+
106
+ Standard JWT tokens are used to authenticate requests from the iframe back to the add-on service. A route can be secured
107
+ using the following code:
108
+
109
+ ```ruby
110
+ include AtlassianJwtAuthentication
111
+
112
+ before_filter only: [:protected] do |controller|
113
+ controller.send(:verify_jwt, 'your-add-on-key', skip_qsh_verification: true)
114
+ end
115
+ ```
116
+
117
+ 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
118
+ by using the standard `jwt` query parameter:
119
+
120
+ ```html
121
+ <a href="/protected?jwt={{token}}">See more</a>
122
+ ```
123
+
124
+ The second option is to use the Authorization HTTP header, e.g. for AJAX requests:
125
+
126
+ ```javascript
127
+ beforeSend: function(request) {
128
+ request.setRequestHeader("Authorization", "JWT {{token}}");
129
+ }
130
+ ```
131
+
132
+ You can embed the token anywhere in your iframe content using the `token` content variable. For example, you can embed
133
+ it in a meta tag, from where it can later be read by a script:
134
+
135
+ ```html
136
+ <meta name="token" content="{{token}}">
137
+
138
+ #### Add-on licensing
139
+ If your add-on has a licensing model you can use the `ensure_license` filter to check for a valid license.
140
+ As with the `verify_jwt` filter, this simply responds with an unauthorized header if there is no valid license
141
+ for the installation.
142
+
143
+ ```ruby
144
+ before_filter :ensure_license
145
+ ```
146
+ If your add-on was for free and you're just adding licensing now, you can specify
147
+ the version at which you started charging, ie. the minimum version of the add-on
148
+ for which you require a valid license. Simply include the code below with your version
149
+ string in the controller that includes the other add-on code.
150
+ ```ruby
151
+ def min_licensing_version
152
+ Gem::Version.new('1.0.0')
153
+ end
154
+ ```
155
+
156
+ ### Middleware
157
+
158
+ You can use a middleware to verify JWT tokens (for example in Rails `application.rb`):
159
+
160
+ ```ruby
161
+ config.middleware.insert_after ActionDispatch::Session::CookieStore, AtlassianJwtAuthentication::Middleware::VerifyJwtToken, 'your_addon_key'
162
+ ```
163
+
164
+ Token will be taken from params or `Authorization` header, if it's verified successfully request will have following headers set:
165
+
166
+ * atlassian_jwt_authorization.jwt_token `JwtToken` instance
167
+ * atlassian_jwt_authorization.account_id `String` instance
168
+ * atlassian_jwt_authorization.context `Hash` instance
169
+
170
+ Middleware will not block requests with invalid or missing JWT tokens, you need to use another layer for that.
171
+
172
+ ### Making a service call
173
+
174
+ Build the URL required to make a service call with the `rest_api_url` helper or
175
+ make a service call with the `rest_api_call` helper that will handle the request for you.
176
+ Both require the method and the endpoint that you need to access:
177
+
178
+ ```ruby
179
+ # Get available project types
180
+ url = rest_api_url(:get, '/rest/api/2/project/type')
181
+ response = Faraday.get(url)
182
+
183
+ # Create an issue
184
+ data = {
185
+ fields: {
186
+ project: {
187
+ 'id': 10100
188
+ },
189
+ summary: 'This is an issue summary',
190
+ issuetype: {
191
+ id: 10200
192
+ }
193
+ }
194
+ }
195
+
196
+ response = rest_api_call(:post, '/rest/api/2/issue', data)
197
+ pp response.success?
198
+
199
+ ```
200
+
201
+ ### User impersonification
202
+
203
+ To make requests on user's behalf add `act_as_user` in scopes required by your app.
204
+
205
+ Later you can obtain [OAuth bearer token](https://developer.atlassian.com/cloud/jira/software/oauth-2-jwt-bearer-token-authorization-grant-type/) from Atlassian.
206
+
207
+ Do that using `AtlassianJwtAuthentication::UserBearerToken.user_bearer_token(account_id, scopes)`
208
+
209
+ ### Logging
210
+
211
+ If you want to debug the JWT verification define a logger in the controller where you're including `AtlassianJwtAuthentication`:
212
+
213
+ ```ruby
214
+ def logger
215
+ Logger.new("#{Rails.root}/log/atlassian_jwt.log")
216
+ end
217
+ ```
218
+
219
+ ### Custom error handling
220
+
221
+ If you want to render your own pages when the add-on throws one of the following errors:
222
+ - forbidden
223
+ - unauthorized
224
+ - payment_required
225
+
226
+ overwrite the following methods in your controller:
227
+
228
+ ```ruby
229
+ def render_forbidden
230
+ # do your own handling there
231
+ # render your own template
232
+ render(template: '...', layout: '...')
233
+ end
234
+
235
+ # the same for render_payment_required and render_unauthorized
236
+
237
+ ```
238
+
239
+ ## Installing the add-on
240
+
241
+ You can use rake tasks to simplify plugin installation:
242
+
243
+ ```ruby
244
+ bin/rails atlassian:install[prefix,email,api_token,https://external.address.to/descriptor]
245
+ ```
246
+
247
+ Where `prefix` is your instance name before `.atlassian.net`. You an get an [API token](https://confluence.atlassian.com/cloud/api-tokens-938839638.html) from [Manage your account](https://id.atlassian.com/manage/api-tokens) page.
248
+
249
+ ## Configuration
250
+
251
+ Config | Environment variable | Description | Default |
252
+ ------ | -------------------- | ----------- | ------- |
253
+ `AtlassianJwtAuthentication.context_path` | none | server path your app is running at | `''`
254
+ `AtlassianJwtAuthentication.verify_jwt_expiration` | `JWT_VERIFY_EXPIRATION` | when `false` allow expired tokens, speeds up development, especially combined with webpack hot module reloading | `true`
255
+ `AtlassianJwtAuthentication.log_requests` | `AJA_LOG_REQUESTS` | when `true` outgoing HTTP requests will be logged | `false`
256
+ `AtlassianJwtAuthentication.debug_requests` | `AJA_DEBUG_REQUESTS` | when `true` HTTP requests will include body content, implicitly turns on `log_requests` | `false`
257
+ `AtlassianJwtAuthentication.signed_install` | `AJA_SIGNED_INSTALL` | Installation lifecycle security improvements. Migration process described [here](https://community.developer.atlassian.com/t/action-required-atlassian-connect-installation-lifecycle-security-improvements/49046). In the descriptor set `"apiMigrations":{"signed-install":AtlassianJwtAuthentication.signed_install}` | `false`
258
+
259
+ ## Requirements
260
+
261
+ Ruby 2.0+, ActiveRecord 4.1+
262
+
263
+ ## Integrations
264
+
265
+ ### Message Bus
266
+
267
+ With middleware enabled you can use following configuration to limit access to message bus per user / instance:
268
+ ```ruby
269
+ MessageBus.user_id_lookup do |env|
270
+ env.try(:[], 'atlassian_jwt_authentication.account_id')
271
+ end
272
+
273
+ MessageBus.site_id_lookup do |env|
274
+ env.try(:[], 'atlassian_jwt_authentication.jwt_token').try(:id)
275
+ end
276
+ ```
277
+
278
+ Then use `MessageBus.publish('/test', 'message', site_id: X, user_ids: [Y])` to publish message only for a user.
279
+
280
+ Requires message_bus patch available at https://github.com/HeroCoders/message_bus/commit/cd7c752fe85a17f7e54aa950a94d7c6378a55ed1
281
+
282
+
283
+ ## Upgrade guide
284
+
285
+ ### Version 0.7.x
286
+
287
+ Removed `current_jwt_user`, `JwtUser`, update your code to use `current_account_id`
288
+
289
+ ### Versions < 0.6.x
290
+
291
+ `current_jwt_auth` has been renamed to `current_jwt_token` to match model name. Either mass rename or add `alias` in your controller:
292
+
293
+ ```ruby
294
+ alias_method :current_jwt_auth, :current_jwt_token
295
+ helper_method :current_jwt_auth
296
+ ```
@@ -0,0 +1,17 @@
1
+ module AtlassianJwtAuthentication
2
+ module ErrorProcessor
3
+ protected
4
+
5
+ def render_forbidden
6
+ head(:forbidden)
7
+ end
8
+
9
+ def render_payment_required
10
+ head(:payment_required)
11
+ end
12
+
13
+ def render_unauthorized
14
+ head(:unauthorized)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,174 @@
1
+ require 'jwt'
2
+
3
+ module AtlassianJwtAuthentication
4
+ module Filters
5
+ protected
6
+
7
+ def on_add_on_installed
8
+ # Add-on key that was installed into the Atlassian Product,
9
+ # as it appears in your add-on's descriptor.
10
+ addon_key = params[:key]
11
+
12
+ # Identifying key for the Atlassian product instance that the add-on was installed into.
13
+ # This will never change for a given instance, and is unique across all Atlassian product tenants.
14
+ # This value should be used to key tenant details in your add-on.
15
+ client_key = params[:clientKey]
16
+
17
+ # Use this string to sign outgoing JWT tokens and validate incoming JWT tokens.
18
+ shared_secret = params[:sharedSecret]
19
+
20
+ # Identifies the category of Atlassian product, e.g. Jira or Confluence.
21
+ product_type = params[:productType]
22
+
23
+ # The base URL of the instance
24
+ base_url = params[:baseUrl]
25
+ api_base_url = params[:baseApiUrl] || base_url
26
+
27
+ jwt_auth = JwtToken.where(client_key: client_key, addon_key: addon_key).first
28
+ if jwt_auth
29
+ # The add-on was previously installed on this client
30
+ return false unless _verify_jwt(addon_key)
31
+ if jwt_auth.id != current_jwt_token.id
32
+ # Update request was issued to another plugin
33
+ render_forbidden
34
+ return false
35
+ end
36
+ else
37
+ self.current_jwt_token = JwtToken.new(jwt_token_params)
38
+ end
39
+
40
+ current_jwt_token.addon_key = addon_key
41
+ current_jwt_token.shared_secret = shared_secret
42
+ current_jwt_token.product_type = "atlassian:#{product_type}"
43
+ current_jwt_token.base_url = base_url if current_jwt_token.respond_to?(:base_url)
44
+ current_jwt_token.api_base_url = api_base_url if current_jwt_token.respond_to?(:api_base_url)
45
+ current_jwt_token.oauth_client_id = params[:oauthClientId] if current_jwt_token.respond_to?(:oauth_client_id)
46
+ current_jwt_token.public_key = params[:publicKey] if current_jwt_token.respond_to?(:public_key)
47
+ current_jwt_token.sen = params[:supportEntitlementNumber] if current_jwt_token.respond_to?(:sen)
48
+ current_jwt_token.payload = params.to_unsafe_h if current_jwt_token.respond_to?(:payload)
49
+
50
+ current_jwt_token.save!
51
+
52
+ true
53
+ end
54
+
55
+ def on_add_on_uninstalled
56
+ addon_key = params[:key]
57
+
58
+ return unless _verify_jwt(addon_key)
59
+
60
+ client_key = params[:clientKey]
61
+
62
+ return false unless client_key.present?
63
+
64
+ JwtToken.where(client_key: client_key, addon_key: addon_key).destroy_all
65
+
66
+ true
67
+ end
68
+
69
+ def verify_jwt(addon_key, skip_qsh_verification: false)
70
+ _verify_jwt(addon_key, true, skip_qsh_verification: skip_qsh_verification)
71
+ end
72
+
73
+ def ensure_license
74
+ unless current_jwt_token
75
+ raise 'current_jwt_token missing, add the verify_jwt filter'
76
+ end
77
+
78
+ response = rest_api_call(:get, "/rest/atlassian-connect/1/addons/#{current_jwt_token.addon_key}")
79
+ unless response.success? && response.data
80
+ log(:error, "Client #{current_jwt_token.client_key}: API call to get the license failed with #{response.status}")
81
+ render_payment_required
82
+ return false
83
+ end
84
+
85
+ current_version = Gem::Version.new(response.data['version'])
86
+
87
+ if min_licensing_version && current_version > min_licensing_version || !min_licensing_version
88
+ # do we need to check for licensing on this add-on version?
89
+ unless params[:lic] && params[:lic] == 'active'
90
+ log(:error, "Client #{current_jwt_token.client_key}: no active license was found in the params")
91
+ render_payment_required
92
+ return false
93
+ end
94
+
95
+ unless response.data['state'] == 'ENABLED' &&
96
+ response.data['license'] && response.data['license']['active']
97
+ log(:error, "client #{current_jwt_token.client_key}: no active & enabled license was found")
98
+ render_payment_required
99
+ return false
100
+ end
101
+ end
102
+
103
+ log(:info, "Client #{current_jwt_token.client_key}: license OK")
104
+
105
+ true
106
+ end
107
+
108
+ private
109
+
110
+ def _verify_jwt(addon_key, consider_param = false, skip_qsh_verification: false)
111
+ self.current_jwt_token = nil
112
+ self.current_account_id = nil
113
+ self.current_jwt_context = nil
114
+
115
+ jwt = nil
116
+
117
+ # The JWT token can be either in the Authorization header
118
+ # or can be sent as a parameter. During the installation
119
+ # handshake we only accept the token coming in the header
120
+ if consider_param
121
+ jwt = params[:jwt] if params[:jwt].present?
122
+ elsif !request.headers['authorization'].present?
123
+ log(:error, 'Missing authorization header')
124
+ render_unauthorized
125
+ return false
126
+ end
127
+
128
+ if request.headers['authorization'].present?
129
+ algorithm, possible_jwt = request.headers['authorization'].split(' ')
130
+ jwt = possible_jwt if algorithm == 'JWT'
131
+ end
132
+
133
+ jwt_verification = AtlassianJwtAuthentication::JWTVerification.new(addon_key, nil, jwt, request)
134
+ jwt_verification.exclude_qsh_params = exclude_qsh_params
135
+ jwt_verification.logger = logger if defined?(logger)
136
+
137
+ jwt_auth, account_id, context, qsh_verified = jwt_verification.verify
138
+
139
+ unless jwt_auth && (qsh_verified || skip_qsh_verification)
140
+ render_unauthorized
141
+ return false
142
+ end
143
+
144
+ self.current_jwt_token = jwt_auth
145
+ self.current_account_id = account_id
146
+ self.current_jwt_context = context
147
+
148
+ true
149
+ end
150
+
151
+ def jwt_token_params
152
+ {
153
+ client_key: params.permit(:clientKey)['clientKey'],
154
+ addon_key: params.permit(:key)['key']
155
+ }
156
+ end
157
+
158
+ # This can be overwritten in the including controller
159
+ def exclude_qsh_params
160
+ []
161
+ end
162
+
163
+ # This can be overwritten in the including controller
164
+ def min_licensing_version
165
+ nil
166
+ end
167
+
168
+ def log(level, message)
169
+ return unless defined?(logger)
170
+
171
+ logger.send(level.to_sym, message)
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,102 @@
1
+ require 'jwt'
2
+ require 'addressable'
3
+
4
+ require_relative './http_client'
5
+
6
+ module AtlassianJwtAuthentication
7
+ module Helper
8
+ protected
9
+
10
+ # Returns the current JWT auth object if it exists
11
+ def current_jwt_token
12
+ @jwt_auth ||= session[:jwt_auth] ? JwtToken.where(id: session[:jwt_auth]).first : nil
13
+ end
14
+
15
+ # Sets the current JWT auth object
16
+ def current_jwt_token=(jwt_auth)
17
+ session[:jwt_auth] = jwt_auth.nil? ? nil : jwt_auth.id
18
+ @jwt_auth = jwt_auth
19
+ end
20
+
21
+ # Returns the current JWT context if it exists
22
+ def current_jwt_context
23
+ @jwt_context ||= session[:jwt_context]
24
+ end
25
+
26
+ # Sets the current JWT context
27
+ def current_jwt_context=(jwt_context)
28
+ session[:jwt_context] = jwt_context
29
+ @jwt_context = jwt_context
30
+ end
31
+
32
+ # Returns the current JWT account_id if it exists
33
+ def current_account_id
34
+ @account_id ||= session[:account_id]
35
+ end
36
+
37
+ # Sets the current JWT account_id
38
+ def current_account_id=(account_id)
39
+ session[:account_id] = account_id
40
+ @account_id = account_id
41
+ end
42
+
43
+ def user_bearer_token(account_id, scopes)
44
+ AtlassianJwtAuthentication::UserBearerToken::user_bearer_token(current_jwt_token, account_id, scopes)
45
+ end
46
+
47
+ def rest_api_url(method, endpoint)
48
+ unless current_jwt_token
49
+ raise 'Missing Authentication context'
50
+ end
51
+
52
+ # Expiry for the JWT token is 3 minutes from now
53
+ issued_at = Time.now.utc.to_i
54
+ expires_at = issued_at + 180
55
+
56
+ qsh = Digest::SHA256.hexdigest("#{method.to_s.upcase}&#{endpoint}&")
57
+
58
+ jwt = JWT.encode({
59
+ qsh: qsh,
60
+ iat: issued_at,
61
+ exp: expires_at,
62
+ iss: current_jwt_token.addon_key
63
+ }, current_jwt_token.shared_secret)
64
+
65
+ # return the service call URL with the JWT token added
66
+ "#{current_jwt_token.api_base_url}#{endpoint}?jwt=#{jwt}"
67
+ end
68
+
69
+ def rest_api_call(method, endpoint, data = nil)
70
+ url = rest_api_url(method, endpoint)
71
+
72
+ response = HttpClient.new(url).send(method) do |request|
73
+ request.body = data ? data.to_json : nil
74
+ request.headers['Content-Type'] = 'application/json'
75
+ end
76
+
77
+ to_json_response(response)
78
+ end
79
+
80
+ def to_json_response(response)
81
+ data = JSON::parse(response.body) rescue nil
82
+ Response.new(response.status, data)
83
+ end
84
+
85
+ class Response
86
+ attr_accessor :status, :data
87
+
88
+ def initialize(status, data = nil)
89
+ @status = status
90
+ @data = data
91
+ end
92
+
93
+ def success?
94
+ status == 200
95
+ end
96
+
97
+ def failed?
98
+ !success?
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,21 @@
1
+ require 'faraday'
2
+
3
+ module AtlassianJwtAuthentication
4
+ module HttpClient
5
+ def self.new(url = nil, options = nil)
6
+ client = Faraday.new(url, options) do |f|
7
+ if AtlassianJwtAuthentication.debug_requests || AtlassianJwtAuthentication.log_requests
8
+ f.response :logger, nil, bodies: AtlassianJwtAuthentication.debug_requests
9
+ end
10
+ f.request :url_encoded
11
+ f.adapter Faraday.default_adapter
12
+ end
13
+
14
+ if block_given?
15
+ yield client
16
+ end
17
+
18
+ client
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,58 @@
1
+ module AtlassianJwtAuthentication
2
+ module Middleware
3
+ class VerifyJwtToken
4
+ PREFIX = 'atlassian_jwt_authentication'.freeze
5
+
6
+ JWT_TOKEN_HEADER = "#{PREFIX}.jwt_token".freeze
7
+ JWT_CONTEXT = "#{PREFIX}.context".freeze
8
+ JWT_ACCOUNT_ID = "#{PREFIX}.account_id".freeze
9
+ JWT_QSH_VERIFIED = "#{PREFIX}.qsh_verified".freeze
10
+
11
+ def initialize(app, options)
12
+ @app = app
13
+ @addon_key = options[:addon_key]
14
+ @if = options[:if]
15
+ @audience = options[:audience]
16
+ end
17
+
18
+ def call(env)
19
+ # Skip if @if predicate is given and evaluates to false
20
+ if @if && !@if.call(env)
21
+ return @app.call(env)
22
+ end
23
+
24
+ request = ActionDispatch::Request.new(env)
25
+
26
+ jwt = request.params[:jwt]
27
+
28
+ if request.headers['authorization'].present?
29
+ algorithm, possible_jwt = request.headers['authorization'].split(' ')
30
+ jwt = possible_jwt if algorithm == 'JWT'
31
+ end
32
+
33
+ if jwt
34
+ jwt_verification = JWTVerification.new(@addon_key, @audience, jwt, request)
35
+ jwt_auth, account_id, context, qsh_verified = jwt_verification.verify
36
+
37
+ if jwt_auth
38
+ request.set_header(JWT_TOKEN_HEADER, jwt_auth)
39
+ end
40
+
41
+ if account_id
42
+ request.set_header(JWT_ACCOUNT_ID, account_id)
43
+ end
44
+
45
+ if context
46
+ request.set_header(JWT_CONTEXT, context)
47
+ end
48
+
49
+ if qsh_verified
50
+ request.set_header(JWT_QSH_VERIFIED, qsh_verified)
51
+ end
52
+ end
53
+
54
+ @app.call(env)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,49 @@
1
+ require_relative './http_client'
2
+
3
+ module AtlassianJwtAuthentication
4
+ module Oauth2
5
+ EXPIRE_IN_SECONDS = 60
6
+ AUTHORIZATION_SERVER_URL = "https://auth.atlassian.io"
7
+ JWT_CLAIM_PREFIX = "urn:atlassian:connect"
8
+ GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
9
+ SCOPE_SEPARATOR = ' '
10
+
11
+ def self.get_access_token(current_jwt_token, account_id, scopes = nil)
12
+ response = HttpClient.new.post(AUTHORIZATION_SERVER_URL + "/oauth2/token") do |request|
13
+ request.headers['Content-Type'] = Faraday::Request::UrlEncoded.mime_type
14
+ request.body = {
15
+ grant_type: GRANT_TYPE,
16
+ assertion: prepare_jwt_token(current_jwt_token, account_id),
17
+ scopes: scopes&.join(SCOPE_SEPARATOR)&.upcase,
18
+ }.compact
19
+ end
20
+
21
+ raise "Request failed with #{response.status}" unless response.success?
22
+
23
+ JSON.parse(response.body)
24
+ end
25
+
26
+ def self.prepare_jwt_token(current_jwt_token, account_id)
27
+ unless current_jwt_token
28
+ raise 'Missing Authentication context'
29
+ end
30
+
31
+ unless account_id
32
+ raise 'Missing User key'
33
+ end
34
+
35
+ # Expiry for the JWT token is 3 minutes from now
36
+ issued_at = Time.now.utc.to_i
37
+ expires_at = issued_at + EXPIRE_IN_SECONDS
38
+
39
+ JWT.encode({
40
+ iss: JWT_CLAIM_PREFIX + ":clientid:" + current_jwt_token.oauth_client_id,
41
+ sub: JWT_CLAIM_PREFIX + ":useraccountid:" + account_id,
42
+ tnt: current_jwt_token.base_url,
43
+ aud: AUTHORIZATION_SERVER_URL,
44
+ iat: issued_at,
45
+ exp: expires_at,
46
+ }, current_jwt_token.shared_secret)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,7 @@
1
+ module AtlassianJwtAuthentication
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ require 'tasks/install'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ module AtlassianJwtAuthentication
2
+ module UserBearerToken
3
+ def self.user_bearer_token(current_jwt_token, account_id, scopes)
4
+ scopes_key = (scopes || []).map(&:downcase).sort.uniq.join(',')
5
+ cache_key = "jwt_token/#{current_jwt_token.id}/user/#{account_id}:scopes:/#{scopes_key}"
6
+
7
+ read_from_cache = ->(refresh = false) do
8
+ Rails.cache.fetch(cache_key, force: refresh) do
9
+ AtlassianJwtAuthentication::Oauth2::get_access_token(current_jwt_token, account_id, scopes).tap do |token_details|
10
+ token_details["expires_at"] = Time.now.utc.to_i + token_details["expires_in"] - 3.seconds # some leeway
11
+ end
12
+ end
13
+ end
14
+
15
+ access_token = read_from_cache.call(false)
16
+ if access_token["expires_at"] <= Time.now.utc.to_i
17
+ access_token = read_from_cache.call(true)
18
+ end
19
+ access_token
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,142 @@
1
+ require 'jwt'
2
+
3
+ module AtlassianJwtAuthentication
4
+ class JWTVerification
5
+ attr_accessor :addon_key, :jwt, :audience, :request, :exclude_qsh_params, :logger
6
+
7
+ def initialize(addon_key, audience, jwt, request, &block)
8
+ self.addon_key = addon_key
9
+ self.audience = audience
10
+ self.jwt = jwt
11
+ self.request = request
12
+
13
+ self.exclude_qsh_params = []
14
+ self.logger = nil
15
+
16
+ yield self if block_given?
17
+ end
18
+
19
+ def verify
20
+ unless jwt.present? && addon_key.present?
21
+ return false
22
+ end
23
+
24
+ # First decode the token without signature & claims verification
25
+ begin
26
+ decoded = JWT.decode(jwt, nil, false, { verify_expiration: AtlassianJwtAuthentication.verify_jwt_expiration })
27
+ rescue => e
28
+ log(:error, "Could not decode JWT: #{e.to_s} \n #{e.backtrace.join("\n")}")
29
+ return false
30
+ end
31
+
32
+ # Extract the data
33
+ data = decoded[0]
34
+ encoding_data = decoded[1]
35
+
36
+ # Find a matching JWT token in the DB
37
+ jwt_auth = JwtToken.where(
38
+ client_key: data['iss'],
39
+ addon_key: addon_key
40
+ ).first
41
+
42
+ unless jwt_auth
43
+ log(:error, "Could not find jwt_token for client_key #{data['iss']} and addon_key #{addon_key}")
44
+ return false
45
+ end
46
+
47
+ # Discard the tokens without verification
48
+ if encoding_data['alg'] == 'none'
49
+ log(:error, "The JWT checking algorithm was set to none for client_key #{data['iss']} and addon_key #{addon_key}")
50
+ return false
51
+ end
52
+
53
+ if AtlassianJwtAuthentication.signed_install && encoding_data['alg'] == 'RS256'
54
+ response = Faraday.get("https://connect-install-keys.atlassian.com/#{encoding_data['kid']}")
55
+ unless response.success? && response.body
56
+ log(:error, "Error retrieving atlassian public key. Response code #{response.status} and kid #{encoding_data['kid']}")
57
+ return false
58
+ end
59
+
60
+ decode_key = OpenSSL::PKey::RSA.new(response.body)
61
+ decode_options = {algorithms: ['RS256'], verify_aud: true, aud: audience}
62
+ else
63
+ decode_key = jwt_auth.shared_secret
64
+ decode_options = {}
65
+ end
66
+
67
+ # Decode the token again, this time with signature & claims verification
68
+ options = JWT::DefaultOptions::DEFAULT_OPTIONS.merge(verify_expiration: AtlassianJwtAuthentication.verify_jwt_expiration).merge(decode_options)
69
+ decoder = JWT::Decode.new(jwt, decode_key, true, options)
70
+ begin
71
+ payload, header = decoder.decode_segments
72
+ rescue JWT::VerificationError
73
+ log(:error, "Error decoding JWT segments - signature is invalid")
74
+ return false
75
+ rescue JWT::ExpiredSignature
76
+ log(:error, "Error decoding JWT segments - signature is expired at #{data['exp']}")
77
+ return false
78
+ end
79
+
80
+ unless header && payload
81
+ log(:error, "Error decoding JWT segments - no header and payload for client_key #{data['iss']} and addon_key #{addon_key}")
82
+ return false
83
+ end
84
+
85
+ if data['qsh']
86
+ # Verify the query has not been tampered by Creating a Query Hash and
87
+ # comparing it against the qsh claim on the verified token
88
+ if jwt_auth.base_url.present? && request.url.include?(jwt_auth.base_url)
89
+ path = request.url.gsub(jwt_auth.base_url, '')
90
+ else
91
+ path = request.path.gsub(AtlassianJwtAuthentication::context_path, '')
92
+ end
93
+ path = '/' if path.empty?
94
+
95
+ qsh_parameters = request.query_parameters.except(:jwt)
96
+
97
+ exclude_qsh_params.each { |param_name| qsh_parameters = qsh_parameters.except(param_name) }
98
+
99
+ qsh = request.method.upcase + '&' + path + '&' +
100
+ qsh_parameters.
101
+ sort.
102
+ map{ |param_pair| encode_param(param_pair) }.
103
+ join('&')
104
+
105
+ qsh = Digest::SHA256.hexdigest(qsh)
106
+
107
+ qsh_verified = data['qsh'] == qsh
108
+ else
109
+ qsh_verified = false
110
+ end
111
+
112
+ context = data['context']
113
+
114
+ # In the case of Confluence and Jira we receive user information inside the JWT token
115
+ if data['context'] && data['context']['user']
116
+ account_id = data['context']['user']['accountId']
117
+ else
118
+ account_id = data['sub']
119
+ end
120
+
121
+ [jwt_auth, account_id, context, qsh_verified]
122
+ end
123
+
124
+ private
125
+
126
+ def encode_param(param_pair)
127
+ key, value = param_pair
128
+
129
+ if value.respond_to?(:to_query)
130
+ value.to_query(key)
131
+ else
132
+ ERB::Util.url_encode(key) + '=' + ERB::Util.url_encode(value)
133
+ end
134
+ end
135
+
136
+ def log(level, message)
137
+ return if logger.nil?
138
+
139
+ logger.send(level.to_sym, message)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,18 @@
1
+ module AtlassianJwtAuthentication
2
+ MAJOR_VERSION = "0"
3
+ MINOR_VERSION = "9"
4
+ PATH_VERSION = "0"
5
+
6
+ # rubygems don't support semantic versioning - https://github.com/rubygems/rubygems/issues/592, using GITHUB_RUN_NUMBER to represent build number
7
+ # going to release pre versions automatically
8
+ BUILD_NUMBER = ENV["GITHUB_RUN_NUMBER"] && ".pre#{ENV["GITHUB_RUN_NUMBER"]}" || ''
9
+
10
+ VERSION =
11
+ (
12
+ [
13
+ MAJOR_VERSION,
14
+ MINOR_VERSION,
15
+ PATH_VERSION
16
+ ].join(".") + BUILD_NUMBER
17
+ ).freeze
18
+ end
@@ -0,0 +1 @@
1
+ require 'atlassian_jwt_authentication'
@@ -0,0 +1,35 @@
1
+ require 'atlassian-jwt-authentication/verify'
2
+ require 'atlassian-jwt-authentication/middleware'
3
+ require 'atlassian-jwt-authentication/filters'
4
+ require 'atlassian-jwt-authentication/error_processor'
5
+ require 'atlassian-jwt-authentication/version'
6
+ require 'atlassian-jwt-authentication/helper'
7
+ require 'atlassian-jwt-authentication/oauth2'
8
+ require 'atlassian-jwt-authentication/user_bearer_token'
9
+ require 'atlassian-jwt-authentication/railtie' if defined?(Rails)
10
+
11
+ module AtlassianJwtAuthentication
12
+ include Helper
13
+ include Filters
14
+ include ErrorProcessor
15
+
16
+ mattr_accessor :context_path
17
+ self.context_path = ''
18
+
19
+ # Decode the JWT parameter without verification
20
+ mattr_accessor :verify_jwt_expiration
21
+ self.verify_jwt_expiration = ENV.fetch('JWT_VERIFY_EXPIRATION', 'true') != 'false'
22
+
23
+ # Log external HTTP requests?
24
+ mattr_accessor :log_requests
25
+ self.log_requests = ENV.fetch('AJA_LOG_REQUESTS', 'false') == 'true'
26
+
27
+ # Debug external HTTP requests? Log bodies
28
+ mattr_accessor :debug_requests
29
+ self.debug_requests = ENV.fetch('AJA_DEBUG_REQUESTS', 'false') == 'true'
30
+
31
+ # installation lifecycle security improvements
32
+ # https://community.developer.atlassian.com/t/action-required-atlassian-connect-installation-lifecycle-security-improvements/49046
33
+ mattr_accessor :signed_install
34
+ self.signed_install = ENV.fetch('AJA_SIGNED_INSTALL', false)
35
+ end
@@ -0,0 +1,38 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module AtlassianJwtAuthentication
4
+ class SetupGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+ desc 'Create a migration to add atlassian jwt specific fields to your model.'
7
+ argument :database_name, required: false,
8
+ type: :string,
9
+ desc: 'Additional database name configuration, if different from `database.yml`'
10
+
11
+ def self.source_root
12
+ @source_root ||= File.expand_path('../templates', __FILE__)
13
+ end
14
+
15
+ def self.next_migration_number(dirname)
16
+ next_migration_number = current_migration_number(dirname) + 1
17
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
18
+ end
19
+
20
+ def self.current_migration_number(dirname) #:nodoc:
21
+ migration_lookup_at(dirname).collect do |file|
22
+ File.basename(file).split('_').first.to_i
23
+ end.max.to_i
24
+ end
25
+
26
+ def self.migration_lookup_at(dirname) #:nodoc:
27
+ Dir.glob("#{dirname}/[0-9]*_*.rb")
28
+ end
29
+
30
+ def generate_migration
31
+ migration_template 'jwt_tokens_migration.rb.erb', "db/#{database_name.present? ? "db_#{database_name}/" : ''}migrate/create_atlassian_jwt_tokens.rb"
32
+ end
33
+
34
+ def generate_models
35
+ template 'jwt_token.rb.erb', File.join('app/models', '', 'jwt_token.rb')
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ class JwtToken < ActiveRecord::Base
2
+ <% if database_name.present? %>
3
+ databases = YAML::load(IO.read('config/database_<%= database_name %>.yml'))
4
+ establish_connection databases[Rails.env]
5
+ <% end %>
6
+ end
@@ -0,0 +1,20 @@
1
+ class CreateAtlassianJwtTokens < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def self.up
3
+ if table_exists? :jwt_tokens
4
+ pp 'Skipping jwt_tokens table creation...'
5
+ else
6
+ create_table :jwt_tokens do |t|
7
+ t.string :addon_key
8
+ t.string :client_key
9
+ t.string :shared_secret
10
+ t.string :product_type
11
+ t.string :base_url
12
+ t.string :api_base_url
13
+ t.string :oauth_client_id
14
+ t.string :public_key
15
+ end
16
+
17
+ add_index(:jwt_tokens, :client_key)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,55 @@
1
+ require 'atlassian-jwt-authentication/http_client'
2
+
3
+ namespace :atlassian do
4
+ desc 'Install plugin descriptor into Atlassian Cloud product'
5
+ task :install, :prefix, :email, :api_token, :descriptor_url do |task, args|
6
+ require 'faraday'
7
+ require 'json'
8
+
9
+ connection =
10
+ AtlassianJwtAuthentication::HttpClient.new("https://#{args.prefix}.atlassian.net") do |f|
11
+ f.basic_auth args.email, args.api_token
12
+ end
13
+
14
+ def check_status(connection, status)
15
+ if status['userInstalled']
16
+ puts 'Plugin was successfully installed'
17
+
18
+ elsif status.fetch('status', {})['done']
19
+ if status.fetch('status', {})['subCode']
20
+ puts "Error installing the plugin #{status['status']['subCode']}"
21
+ else
22
+ puts 'Plugin was successfully installed'
23
+ end
24
+
25
+ else
26
+ wait_for = [status['pingAfter'], 5].min
27
+ puts "waiting #{wait_for} seconds for plugin to load..."
28
+ sleep(wait_for)
29
+
30
+ response = connection.get(status['links']['self'])
31
+
32
+ if response.status == 303
33
+ puts 'Plugin was successfully installed'
34
+ return
35
+ end
36
+
37
+ check_status(connection, JSON.parse(response.body))
38
+ end
39
+ end
40
+
41
+ response = connection.get("/rest/plugins/1.0/")
42
+ if response.success?
43
+ token = response.headers['upm-token']
44
+
45
+ response = connection.post("/rest/plugins/1.0/", {pluginUri: args.descriptor_url}.to_json, 'Content-Type' => 'application/vnd.atl.plugins.remote.install+json') do |req|
46
+ req.params['token'] = token
47
+ end
48
+
49
+ payload = JSON.parse(response.body)
50
+ check_status(connection, payload)
51
+ else
52
+ puts "Cannot get UPM token: #{response.status}"
53
+ end
54
+ end
55
+ end
metadata ADDED
@@ -0,0 +1,174 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: atlassian-jwt-authentication
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0.pre8
5
+ platform: ruby
6
+ authors:
7
+ - Laura Barladeanu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-10-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: addressable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 2.4.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 2.4.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0.11'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: jwt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.2.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.2.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 4.1.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 4.1.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: generator_spec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: 'Atlassian JWT Authentication provides support for handling JWT authentication
126
+ as required by Atlassian when building add-ons: https://developer.atlassian.com/static/connect/docs/latest/concepts/authentication.html'
127
+ email: laura@meisterlabs.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - CHANGELOG
133
+ - MIT-LICENSE
134
+ - README.md
135
+ - lib/atlassian-jwt-authentication.rb
136
+ - lib/atlassian-jwt-authentication/error_processor.rb
137
+ - lib/atlassian-jwt-authentication/filters.rb
138
+ - lib/atlassian-jwt-authentication/helper.rb
139
+ - lib/atlassian-jwt-authentication/http_client.rb
140
+ - lib/atlassian-jwt-authentication/middleware.rb
141
+ - lib/atlassian-jwt-authentication/oauth2.rb
142
+ - lib/atlassian-jwt-authentication/railtie.rb
143
+ - lib/atlassian-jwt-authentication/user_bearer_token.rb
144
+ - lib/atlassian-jwt-authentication/verify.rb
145
+ - lib/atlassian-jwt-authentication/version.rb
146
+ - lib/atlassian_jwt_authentication.rb
147
+ - lib/generators/atlassian_jwt_authentication/setup_generator.rb
148
+ - lib/generators/atlassian_jwt_authentication/templates/jwt_token.rb.erb
149
+ - lib/generators/atlassian_jwt_authentication/templates/jwt_tokens_migration.rb.erb
150
+ - lib/tasks/install.rb
151
+ homepage: https://meisterlabs.com/
152
+ licenses:
153
+ - MIT
154
+ metadata: {}
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '2'
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">"
167
+ - !ruby/object:Gem::Version
168
+ version: 1.3.1
169
+ requirements: []
170
+ rubygems_version: 3.1.6
171
+ signing_key:
172
+ specification_version: 4
173
+ summary: DB architecture and controller filters for dealing with Atlassian's JWT authentication
174
+ test_files: []