atlassian-jwt-authentication 0.9.0.pre8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []