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 +7 -0
- data/CHANGELOG +0 -0
- data/MIT-LICENSE +20 -0
- data/README.md +296 -0
- data/lib/atlassian-jwt-authentication/error_processor.rb +17 -0
- data/lib/atlassian-jwt-authentication/filters.rb +174 -0
- data/lib/atlassian-jwt-authentication/helper.rb +102 -0
- data/lib/atlassian-jwt-authentication/http_client.rb +21 -0
- data/lib/atlassian-jwt-authentication/middleware.rb +58 -0
- data/lib/atlassian-jwt-authentication/oauth2.rb +49 -0
- data/lib/atlassian-jwt-authentication/railtie.rb +7 -0
- data/lib/atlassian-jwt-authentication/user_bearer_token.rb +22 -0
- data/lib/atlassian-jwt-authentication/verify.rb +142 -0
- data/lib/atlassian-jwt-authentication/version.rb +18 -0
- data/lib/atlassian-jwt-authentication.rb +1 -0
- data/lib/atlassian_jwt_authentication.rb +35 -0
- data/lib/generators/atlassian_jwt_authentication/setup_generator.rb +38 -0
- data/lib/generators/atlassian_jwt_authentication/templates/jwt_token.rb.erb +6 -0
- data/lib/generators/atlassian_jwt_authentication/templates/jwt_tokens_migration.rb.erb +20 -0
- data/lib/tasks/install.rb +55 -0
- metadata +174 -0
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,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,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: []
|