devise-jwt 0.4.4 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +95 -3
- data/devise-jwt.gemspec +1 -1
- data/lib/devise/jwt.rb +4 -0
- data/lib/devise/jwt/revocation_strategies.rb +1 -0
- data/lib/devise/jwt/revocation_strategies/whitelist.rb +51 -0
- data/lib/devise/jwt/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7d9d15287c66562066c4c4a729506396e7ec4007
|
4
|
+
data.tar.gz: 1043788671b10218a3bc7c778f10a2f2c258ee84
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1fb0e64bff0707c6a476868f5b1ed9fe967472e0fa385dac3e7c74b8f54433a98d57b1e16b057e59d566721ab91444aed1ec5ece13e309867c43da5bfe5554c4
|
7
|
+
data.tar.gz: d2cb740987f7c7321a16a38def56fb7e31e90cc4b17f3cdb37ae65f38ac308b64619d52e4ac929f5fbab0ccb18e264837805a98342a9160710df806413b5e9f9
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
6
6
|
|
7
|
+
## [0.5.0] - 2017-12-11
|
8
|
+
### Added
|
9
|
+
- Added whitelist strategy
|
10
|
+
- Update `warden-jwt_auth` dependency
|
11
|
+
|
7
12
|
## [0.4.4] - 2017-12-04
|
8
13
|
### Fixed
|
9
14
|
- Configure classes as strings to avoid problems with Rails STI
|
data/README.md
CHANGED
@@ -26,7 +26,7 @@ You can read about which security concerns this library takes into account and a
|
|
26
26
|
Add this line to your application's Gemfile:
|
27
27
|
|
28
28
|
```ruby
|
29
|
-
gem 'devise-jwt', '~> 0.
|
29
|
+
gem 'devise-jwt', '~> 0.5.0'
|
30
30
|
```
|
31
31
|
|
32
32
|
And then execute:
|
@@ -86,6 +86,14 @@ def jwt_payload
|
|
86
86
|
end
|
87
87
|
```
|
88
88
|
|
89
|
+
You can add a hook method `on_jwt_dispatch` on the user model. It is executed when a token dispatched for that user instance, and it takes `token` and `payload` as parameters.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
def on_jwt_dispatch(token, payload)
|
93
|
+
do_something(token, payload)
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
89
97
|
Note: if you are making cross-domain requests, make sure that you add `Authorization` header to the list of allowed request headers and exposed response headers. You can use something like [rack-cors](https://github.com/cyu/rack-cors) for that, for example:
|
90
98
|
|
91
99
|
```ruby
|
@@ -103,7 +111,7 @@ end
|
|
103
111
|
|
104
112
|
### Revocation strategies
|
105
113
|
|
106
|
-
`devise-jwt` comes with
|
114
|
+
`devise-jwt` comes with three revocation strategies out of the box. Some of them are implementations of what is discussed in the blog post [JWT Revocation Strategies](http://waiting-for-dev.github.io/blog/2017/01/24/jwt_revocation_strategies/), where I also talk about their pros and cons.
|
107
115
|
|
108
116
|
#### JTIMatcher
|
109
117
|
|
@@ -152,7 +160,7 @@ end
|
|
152
160
|
|
153
161
|
#### Blacklist
|
154
162
|
|
155
|
-
In this strategy, a database table is used as a blacklist of revoked JWT tokens. The `jti` claim, which uniquely identifies a token, is persisted.
|
163
|
+
In this strategy, a database table is used as a blacklist of revoked JWT tokens. The `jti` claim, which uniquely identifies a token, is persisted. The `exp` claim is also stored to allow the clean-up of staled tokens.
|
156
164
|
|
157
165
|
In order to use it, you need to create the blacklist table in a migration:
|
158
166
|
|
@@ -197,6 +205,74 @@ class User < ApplicationRecord
|
|
197
205
|
end
|
198
206
|
```
|
199
207
|
|
208
|
+
#### Whitelist
|
209
|
+
|
210
|
+
Here, the model itself acts also as a revocation strategy, but it needs to have
|
211
|
+
a one-to-many association with another table which stores the tokens (in fact
|
212
|
+
their `jti` claim, which uniquely identifies them) valids for each user record.
|
213
|
+
|
214
|
+
The workflow is as the following:
|
215
|
+
|
216
|
+
- Once a token is dispatched for a user, its `jti` claim is stored in the
|
217
|
+
associated table.
|
218
|
+
- At every authentication, the incoming token `jti` is matched against all the
|
219
|
+
`jti` associated to that user. The authentication only succeeds if one of
|
220
|
+
them matches.
|
221
|
+
- On a sign out, the token `jti` is deleted from the associated table.
|
222
|
+
|
223
|
+
In fact, besides the `jti` claim, the `aud` claim is also stored and matched at
|
224
|
+
every authentication. This, together with the [aud_header](#aud_header)
|
225
|
+
configuration parameter, can be used to differentiate between clients or
|
226
|
+
devices for the same user.
|
227
|
+
|
228
|
+
The `exp` claim is also stored to allow the clean-up of staled tokens.
|
229
|
+
|
230
|
+
In order to use it, you have to create yourself the associated table and model.
|
231
|
+
The association table must be called `whitelisted_jwts`:
|
232
|
+
|
233
|
+
```ruby
|
234
|
+
def change
|
235
|
+
create_table :whitelisted_jwts do |t|
|
236
|
+
t.string :jti, null: false
|
237
|
+
t.string :aud
|
238
|
+
# If you want to leverage the `aud` claim, add to it a `NOT NULL` constraint:
|
239
|
+
# t.string :aud, null: false
|
240
|
+
t.datetime :exp, null: false
|
241
|
+
t.references :your_user_table, foreign_key: true
|
242
|
+
end
|
243
|
+
|
244
|
+
add_index :whitelisted_jwts, :jti, unique: true
|
245
|
+
end
|
246
|
+
```
|
247
|
+
Important: You are encouraged to set a unique index in the jti column. This way we can be sure at the database level that there aren't two valid tokens with same jti at the same time.
|
248
|
+
|
249
|
+
And then, the model:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
class WhitelistedJwt < ApplicationRecord
|
253
|
+
end
|
254
|
+
```
|
255
|
+
|
256
|
+
Finally, include de strategy in the model and configure it:
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
class User < ApplicationRecord
|
260
|
+
include Devise::JWT::RevocationStrategies::Whitelist
|
261
|
+
|
262
|
+
devise :database_authenticatable,
|
263
|
+
:jwt_authenticatable, jwt_revocation_strategy: self
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
Be aware that this strategy makes uses of `on_jwt_dispatch` method in the user model, so if you need to use it don't forget to call `super`:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
def on_jwt_dispatch(token, payload)
|
271
|
+
super
|
272
|
+
do_something(token, payload)
|
273
|
+
end
|
274
|
+
```
|
275
|
+
|
200
276
|
#### Null strategy
|
201
277
|
|
202
278
|
A [null object pattern](https://en.wikipedia.org/wiki/Null_Object_pattern) strategy, which does not revoke tokens, is provided out of the box just in case you are absolutely sure you don't need token revocation. It is recommended **not to use it**.
|
@@ -308,6 +384,22 @@ jwt.request_formats = {
|
|
308
384
|
|
309
385
|
By default, only requests without format are processed.
|
310
386
|
|
387
|
+
#### aud_header
|
388
|
+
|
389
|
+
Request header which content will be stored to the `aud` claim in the payload.
|
390
|
+
|
391
|
+
It is used to validate whether an incoming token was originally issued to the
|
392
|
+
same client, checking if `aud` and the `aud_header` header value match. If you
|
393
|
+
don't want to differentiate between clients, you don't need to provide that
|
394
|
+
header.
|
395
|
+
|
396
|
+
**Important:** Be aware that this workflow is not bullet proof. In some
|
397
|
+
scenarios a user can handcraft the request headers, therefore being able to
|
398
|
+
impersonate any client. In such cases you could need something more robust,
|
399
|
+
like an OAuth workflow with client id and client secret.
|
400
|
+
|
401
|
+
Defaults to `JWT_AUD`.
|
402
|
+
|
311
403
|
## Development
|
312
404
|
|
313
405
|
There are docker and docker-compose files configured to create a development environment for this gem. So, if you use Docker you only need to run:
|
data/devise-jwt.gemspec
CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
24
|
spec.add_dependency 'devise', '~> 4.0'
|
25
|
-
spec.add_dependency 'warden-jwt_auth', '~> 0.
|
25
|
+
spec.add_dependency 'warden-jwt_auth', '~> 0.3.0'
|
26
26
|
|
27
27
|
spec.add_development_dependency "bundler", "~> 1.12"
|
28
28
|
spec.add_development_dependency "rake", "~> 10.0"
|
data/lib/devise/jwt.rb
CHANGED
@@ -42,6 +42,10 @@ module Devise
|
|
42
42
|
forward_to_warden(:revocation_requests, value)
|
43
43
|
end
|
44
44
|
|
45
|
+
setting(:aud_header) do |value|
|
46
|
+
forward_to_warden(:aud_header, value)
|
47
|
+
end
|
48
|
+
|
45
49
|
# A hash of warden scopes as keys and an array of request formats that will
|
46
50
|
# be processed as values. When a scope is not present or if it has a nil
|
47
51
|
# item, requests without format will be taken into account.
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module Devise
|
6
|
+
module JWT
|
7
|
+
module RevocationStrategies
|
8
|
+
# This strategy must be included in the user model.
|
9
|
+
#
|
10
|
+
# The JwtWhitelist table must include `jti`, `aud`, `exp` and `user_id`
|
11
|
+
# columns
|
12
|
+
#
|
13
|
+
# In order to tell whether a token is revoked, it just tries to find the
|
14
|
+
# `jti` and `aud` values from the token on the `whitelisted_jwts`
|
15
|
+
# table for the respective user.
|
16
|
+
#
|
17
|
+
# If the values don't exist means the token was revoked.
|
18
|
+
# On revocation, it deletes the matching record from the
|
19
|
+
# `whitelisted_jwts` table.
|
20
|
+
#
|
21
|
+
# On sign in, it creates a new record with the `jti` and `aud` values.
|
22
|
+
module Whitelist
|
23
|
+
extend ActiveSupport::Concern
|
24
|
+
|
25
|
+
included do
|
26
|
+
has_many :whitelisted_jwts, dependent: :destroy
|
27
|
+
|
28
|
+
# @see Warden::JWTAuth::Interfaces::RevocationStrategy#jwt_revoked?
|
29
|
+
def self.jwt_revoked?(payload, user)
|
30
|
+
!user.whitelisted_jwts.exists?(payload.slice('jti', 'aud'))
|
31
|
+
end
|
32
|
+
|
33
|
+
# @see Warden::JWTAuth::Interfaces::RevocationStrategy#revoke_jwt
|
34
|
+
def self.revoke_jwt(payload, user)
|
35
|
+
user.whitelisted_jwts.find_by(payload.slice('jti', 'aud')).destroy!
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Warden::JWTAuth::Interfaces::User#on_jwt_dispatch
|
40
|
+
# :reek:FeatureEnvy
|
41
|
+
def on_jwt_dispatch(_token, payload)
|
42
|
+
whitelisted_jwts.create!(
|
43
|
+
jti: payload['jti'],
|
44
|
+
aud: payload['aud'],
|
45
|
+
exp: Time.at(payload['exp'].to_i)
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/devise/jwt/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: devise-jwt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marc Busqué
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-12-
|
11
|
+
date: 2017-12-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: devise
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.
|
33
|
+
version: 0.3.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0.
|
40
|
+
version: 0.3.0
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: bundler
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -200,6 +200,7 @@ files:
|
|
200
200
|
- lib/devise/jwt/revocation_strategies/blacklist.rb
|
201
201
|
- lib/devise/jwt/revocation_strategies/jti_matcher.rb
|
202
202
|
- lib/devise/jwt/revocation_strategies/null.rb
|
203
|
+
- lib/devise/jwt/revocation_strategies/whitelist.rb
|
203
204
|
- lib/devise/jwt/version.rb
|
204
205
|
homepage: https://github.com/waiting-for-dev/devise-jwt
|
205
206
|
licenses:
|