jwt_keeper 3.2.0 → 5.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/README.md +3 -1
- data/Rakefile +2 -0
- data/jwt_keeper.gemspec +3 -2
- data/lib/generators/templates/jwt_keeper.rb +5 -0
- data/lib/jwt_keeper.rb +0 -2
- data/lib/jwt_keeper/configuration.rb +2 -2
- data/lib/jwt_keeper/controller.rb +3 -3
- data/lib/jwt_keeper/datastore.rb +18 -2
- data/lib/jwt_keeper/token.rb +33 -18
- data/lib/jwt_keeper/version.rb +1 -1
- data/spec/lib/jwt_keeper/controller_spec.rb +2 -1
- data/spec/lib/jwt_keeper/token_spec.rb +77 -7
- data/spec/spec_helper.rb +1 -0
- metadata +28 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ce4912150544b5d9944105ef36f43022c480578eb922d6a5f001c2044a53de6
|
4
|
+
data.tar.gz: 8f1ef7fa13008c88133aac5babbf8622070db55ac3ea49eb45ef515c23e1751b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: df62c535a49f323772ee6b7fe995ada5a1906c1ac3d80916949a30c7030c058eea47105368edc97351c1853bdf11cf6f561452e21d71d2d79f32246964acf3b5
|
7
|
+
data.tar.gz: b03abb09f142d3d5b27706b9792be412ea8c5698b4c7c385a343f15cd147cf9b71202c32b24b7dbbd892a9ac054be6918b9562498f7f8a347d30d054d54ddc19
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -32,11 +32,13 @@ raw_token_string = token.to_jwt
|
|
32
32
|
The designed rails token flow is to receive and respond to requests with the token being present in the `Authorization` part of the header. This is to allow us to seamlessly rotate the tokens on the fly without having to rebuff the request as part of the user flow. Automatic rotation happens as part of the `require_authentication` action, meaning that you will always get the latest token data as created by `generate_claims` in your controllers. This new token is added to the response with the `write_authentication_token` action.
|
33
33
|
|
34
34
|
```bash
|
35
|
-
|
35
|
+
rails generate jwt_keeper:install
|
36
36
|
```
|
37
37
|
|
38
38
|
```ruby
|
39
39
|
class ApplicationController < ActionController::Base
|
40
|
+
include JWTKeeper::Controller
|
41
|
+
|
40
42
|
before_action :require_authentication
|
41
43
|
|
42
44
|
def not_authenticated
|
data/Rakefile
CHANGED
data/jwt_keeper.gemspec
CHANGED
@@ -23,13 +23,14 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_development_dependency 'yard'
|
24
24
|
spec.add_development_dependency 'rubocop'
|
25
25
|
spec.add_development_dependency 'dotenv'
|
26
|
+
spec.add_development_dependency 'pry'
|
26
27
|
|
27
28
|
spec.add_development_dependency 'rspec', '~> 3.8'
|
28
29
|
spec.add_development_dependency 'fuubar'
|
29
30
|
spec.add_development_dependency 'simplecov'
|
30
31
|
|
31
32
|
spec.add_dependency 'redis'
|
32
|
-
spec.add_dependency 'rails'
|
33
|
-
spec.add_dependency 'activesupport'
|
33
|
+
spec.add_dependency 'rails'
|
34
|
+
spec.add_dependency 'activesupport'
|
34
35
|
spec.add_dependency 'jwt', '>= 1.5'
|
35
36
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
JWTKeeper.configure do |config|
|
2
4
|
# The time to expire for the tokens
|
3
5
|
# config.expiry = 1.hour
|
@@ -26,6 +28,9 @@ JWTKeeper.configure do |config|
|
|
26
28
|
|
27
29
|
# the location of redis config file
|
28
30
|
# config.redis_connection = Redis.new(connection_options)
|
31
|
+
# config.redis_connection = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5)) do
|
32
|
+
# Redis.new(url: ENV['REDISCLOUD_URL'] || 'redis://localhost:6379/')
|
33
|
+
# end
|
29
34
|
|
30
35
|
# A unique idenfitier for the token version.
|
31
36
|
# config.version = 1
|
data/lib/jwt_keeper.rb
CHANGED
@@ -29,7 +29,7 @@ module JWTKeeper
|
|
29
29
|
@authentication_token ||=
|
30
30
|
JWTKeeper::Token.find(
|
31
31
|
request.headers['Authorization'].split.last,
|
32
|
-
cookies.signed['jwt_keeper']
|
32
|
+
cookie_secret: defined?(cookies) && cookies.signed['jwt_keeper']
|
33
33
|
)
|
34
34
|
end
|
35
35
|
|
@@ -39,7 +39,7 @@ module JWTKeeper
|
|
39
39
|
def write_authentication_token(token)
|
40
40
|
return clear_authentication_token if token.nil?
|
41
41
|
response.headers['Authorization'] = "Bearer #{token.to_jwt}"
|
42
|
-
cookies.signed['jwt_keeper'] = token.to_cookie
|
42
|
+
defined?(cookies) && cookies.signed['jwt_keeper'] = token.to_cookie
|
43
43
|
@authentication_token = token
|
44
44
|
end
|
45
45
|
|
@@ -47,7 +47,7 @@ module JWTKeeper
|
|
47
47
|
# @return [void]
|
48
48
|
def clear_authentication_token
|
49
49
|
response.headers['Authorization'] = nil
|
50
|
-
cookies.delete('jwt_keeper')
|
50
|
+
defined?(cookies) && cookies.delete('jwt_keeper')
|
51
51
|
@authentication_token = nil
|
52
52
|
end
|
53
53
|
|
data/lib/jwt_keeper/datastore.rb
CHANGED
@@ -27,12 +27,28 @@ module JWTKeeper
|
|
27
27
|
|
28
28
|
# @!visibility private
|
29
29
|
def set_with_expiry(jti, seconds, type)
|
30
|
-
JWTKeeper.configuration.redis_connection
|
30
|
+
redis = JWTKeeper.configuration.redis_connection
|
31
|
+
|
32
|
+
if redis.is_a?(Redis)
|
33
|
+
redis.setex(jti, seconds, type)
|
34
|
+
elsif defined?(ConnectionPool) && redis.is_a?(ConnectionPool)
|
35
|
+
redis.with { |conn| conn.setex(jti, seconds, type) }
|
36
|
+
else
|
37
|
+
throw 'Bad Redis Connection'
|
38
|
+
end
|
31
39
|
end
|
32
40
|
|
33
41
|
# @!visibility private
|
34
42
|
def get(jti)
|
35
|
-
JWTKeeper.configuration.redis_connection
|
43
|
+
redis = JWTKeeper.configuration.redis_connection
|
44
|
+
|
45
|
+
if redis.is_a?(Redis)
|
46
|
+
redis.get(jti)
|
47
|
+
elsif defined?(ConnectionPool) && redis.is_a?(ConnectionPool)
|
48
|
+
redis.with { |conn| conn.get(jti) }
|
49
|
+
else
|
50
|
+
throw 'Bad Redis Connection'
|
51
|
+
end
|
36
52
|
end
|
37
53
|
end
|
38
54
|
end
|
data/lib/jwt_keeper/token.rb
CHANGED
@@ -2,40 +2,47 @@ module JWTKeeper
|
|
2
2
|
# This class acts as the main interface to wrap the concerns of JWTs. Handling everything from
|
3
3
|
# encoding to invalidation.
|
4
4
|
class Token
|
5
|
-
attr_accessor :claims, :cookie_secret
|
5
|
+
attr_accessor :claims, :secret, :cookie_secret
|
6
6
|
|
7
7
|
# Initalizes a new web token
|
8
|
-
# @param
|
9
|
-
# @param
|
8
|
+
# @param options [Hash] the custom claims to encode
|
9
|
+
# @param secret the secret to use during encoding, defaults to config
|
10
|
+
# @param cookie_secret the cookie secret to use during encoding
|
10
11
|
# @return [void]
|
11
|
-
def initialize(
|
12
|
-
@
|
12
|
+
def initialize(options = {})
|
13
|
+
@secret = options.delete(:secret) || JWTKeeper.configuration.secret
|
14
|
+
@cookie_secret = options.delete(:cookie_secret)
|
13
15
|
@claims = {
|
14
16
|
nbf: DateTime.now.to_i, # not before
|
15
17
|
iat: DateTime.now.to_i, # issued at
|
16
18
|
jti: SecureRandom.uuid # JWT ID
|
17
19
|
}
|
20
|
+
|
18
21
|
@claims.merge!(JWTKeeper.configuration.base_claims)
|
19
|
-
@claims.merge!(
|
22
|
+
@claims.merge!(options)
|
23
|
+
@claims[:exp] = @claims[:exp].to_i if @claims[:exp].is_a?(Time)
|
20
24
|
end
|
21
25
|
|
22
26
|
# Creates a new web token
|
23
|
-
# @param
|
27
|
+
# @param options [Hash] the custom claims to encode
|
28
|
+
# @param secret the secret to use during encoding, defaults to config
|
24
29
|
# @return [Token] token object
|
25
|
-
def self.create(
|
30
|
+
def self.create(options)
|
26
31
|
cookie_secret = SecureRandom.hex(16) if JWTKeeper.configuration.cookie_lock
|
27
|
-
new(
|
32
|
+
new(options.merge(cookie_secret: cookie_secret))
|
28
33
|
end
|
29
34
|
|
30
35
|
# Decodes and validates an existing token
|
31
36
|
# @param raw_token [String] the raw token
|
32
37
|
# @param cookie_secret [String] the cookie secret
|
33
38
|
# @return [Token] token object
|
34
|
-
def self.find(raw_token, cookie_secret
|
35
|
-
claims = decode(raw_token, cookie_secret)
|
39
|
+
def self.find(raw_token, secret: nil, cookie_secret: nil, iss: nil)
|
40
|
+
claims = decode(raw_token, secret: secret, cookie_secret: cookie_secret, iss: iss)
|
36
41
|
return nil if claims.nil?
|
37
42
|
|
38
|
-
new_token = new(
|
43
|
+
new_token = new(secret: secret, cookie_secret: cookie_secret, iss: iss)
|
44
|
+
new_token.claims = claims
|
45
|
+
|
39
46
|
return nil if new_token.revoked?
|
40
47
|
new_token
|
41
48
|
end
|
@@ -66,6 +73,7 @@ module JWTKeeper
|
|
66
73
|
# @param new_claims [Hash] Used to override and update claims during rotation
|
67
74
|
# @return [Token]
|
68
75
|
def rotate(new_claims = nil)
|
76
|
+
return self if claims[:iss] != JWTKeeper.configuration.issuer
|
69
77
|
revoke
|
70
78
|
|
71
79
|
new_claims ||= claims.except(:iss, :aud, :exp, :nbf, :iat, :jti)
|
@@ -110,7 +118,11 @@ module JWTKeeper
|
|
110
118
|
# Checks if the token invalid?
|
111
119
|
# @return [Boolean]
|
112
120
|
def invalid?
|
113
|
-
self.class.decode(
|
121
|
+
self.class.decode(
|
122
|
+
encode,
|
123
|
+
secret: secret,
|
124
|
+
cookie_secret: cookie_secret
|
125
|
+
).nil? || revoked?
|
114
126
|
end
|
115
127
|
|
116
128
|
# Encodes the jwt
|
@@ -130,8 +142,11 @@ module JWTKeeper
|
|
130
142
|
end
|
131
143
|
|
132
144
|
# @!visibility private
|
133
|
-
def self.decode(raw_token, cookie_secret)
|
134
|
-
|
145
|
+
def self.decode(raw_token, secret: nil, cookie_secret: nil, iss: nil)
|
146
|
+
secret ||= JWTKeeper.configuration.secret
|
147
|
+
iss ||= JWTKeeper.configuration.issuer
|
148
|
+
|
149
|
+
JWT.decode(raw_token, secret.to_s + cookie_secret.to_s, true,
|
135
150
|
algorithm: JWTKeeper.configuration.algorithm,
|
136
151
|
verify_iss: true,
|
137
152
|
verify_aud: true,
|
@@ -139,7 +154,7 @@ module JWTKeeper
|
|
139
154
|
verify_sub: false,
|
140
155
|
verify_jti: false,
|
141
156
|
leeway: 0,
|
142
|
-
iss:
|
157
|
+
iss: iss,
|
143
158
|
aud: JWTKeeper.configuration.audience
|
144
159
|
).first.symbolize_keys
|
145
160
|
|
@@ -151,8 +166,8 @@ module JWTKeeper
|
|
151
166
|
|
152
167
|
# @!visibility private
|
153
168
|
def encode
|
154
|
-
JWT.encode(claims,
|
155
|
-
|
169
|
+
JWT.encode(claims.compact,
|
170
|
+
secret.to_s + cookie_secret.to_s,
|
156
171
|
JWTKeeper.configuration.algorithm
|
157
172
|
)
|
158
173
|
end
|
data/lib/jwt_keeper/version.rb
CHANGED
@@ -4,7 +4,8 @@ RSpec.describe JWTKeeper do
|
|
4
4
|
describe 'Controller' do
|
5
5
|
include_context 'initialize config'
|
6
6
|
|
7
|
-
let(:token) { JWTKeeper::Token.create(claim: "
|
7
|
+
let(:token) { JWTKeeper::Token.create(claim: "The Earth is Flat") }
|
8
|
+
|
8
9
|
subject(:test_controller) do
|
9
10
|
cookies_klass = Class.new(Hash) do
|
10
11
|
def signed
|
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
module JWTKeeper
|
4
4
|
RSpec.describe Token do
|
5
5
|
include_context 'initialize config'
|
6
|
-
let(:private_claims) { { claim: "
|
6
|
+
let(:private_claims) { { claim: "The Earth is Flat" } }
|
7
7
|
let(:token) { described_class.create(private_claims) }
|
8
8
|
let(:raw_token) { token.to_jwt }
|
9
9
|
|
@@ -24,6 +24,25 @@ module JWTKeeper
|
|
24
24
|
it { is_expected.to be_instance_of described_class }
|
25
25
|
it { expect(subject.claims[:exp]).to eql private_claims[:exp] }
|
26
26
|
end
|
27
|
+
|
28
|
+
context 'when overriding default secret' do
|
29
|
+
subject { described_class.create(**private_claims, secret: secret) }
|
30
|
+
|
31
|
+
let(:secret) { SecureRandom.uuid }
|
32
|
+
|
33
|
+
it { is_expected.to be_instance_of described_class }
|
34
|
+
it { expect(subject.claims[:claim]).to eql private_claims[:claim] }
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'when overriding default issuer' do
|
38
|
+
subject { described_class.create(**private_claims, iss: issuer) }
|
39
|
+
|
40
|
+
let(:issuer) { 'ISSUER' }
|
41
|
+
|
42
|
+
it { is_expected.to be_instance_of described_class }
|
43
|
+
it { expect(subject.claims[:claim]).to eql private_claims[:claim] }
|
44
|
+
it { expect(subject.claims[:iss]).to eql issuer }
|
45
|
+
end
|
27
46
|
end
|
28
47
|
|
29
48
|
describe '.find' do
|
@@ -42,16 +61,50 @@ module JWTKeeper
|
|
42
61
|
it { is_expected.to be nil }
|
43
62
|
end
|
44
63
|
|
45
|
-
context 'with
|
46
|
-
|
47
|
-
|
64
|
+
context 'describe with cookie locking' do
|
65
|
+
before { JWTKeeper.configure(JWTKeeper::Configuration.new(config.merge(cookie_lock: true))) }
|
66
|
+
|
67
|
+
context 'with no cookie' do
|
68
|
+
subject { described_class.find(raw_token, cookie_secret: nil) }
|
69
|
+
it { is_expected.to be nil }
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'with bad cookie' do
|
73
|
+
subject { described_class.find(raw_token, cookie_secret: 'BAD_COOKIE') }
|
74
|
+
it { is_expected.to be nil }
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'with valid cookie' do
|
78
|
+
subject { described_class.find(raw_token, cookie_secret: token.cookie_secret) }
|
79
|
+
it { is_expected.to be_instance_of described_class }
|
80
|
+
end
|
48
81
|
end
|
49
82
|
|
50
|
-
context '
|
51
|
-
|
52
|
-
|
83
|
+
context 'when overriding default secret' do
|
84
|
+
subject { described_class.find(raw_token, secret: secret) }
|
85
|
+
|
86
|
+
let(:token) { described_class.create(**private_claims, secret: secret) }
|
87
|
+
let(:secret) { SecureRandom.uuid }
|
53
88
|
|
54
89
|
it { is_expected.to be_instance_of described_class }
|
90
|
+
it { expect(subject.claims[:claim]).to eql private_claims[:claim] }
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'when overriding default issuer' do
|
94
|
+
subject { described_class.find(raw_token, iss: issuer) }
|
95
|
+
|
96
|
+
let(:token) { described_class.create(**private_claims, iss: issuer) }
|
97
|
+
let(:issuer) { 'ISSUER' }
|
98
|
+
|
99
|
+
it { is_expected.to be_instance_of described_class }
|
100
|
+
it { expect(subject.claims[:claim]).to eql private_claims[:claim] }
|
101
|
+
it { expect(subject.claims[:iss]).to eql issuer }
|
102
|
+
|
103
|
+
context 'with an issuer mismatch' do
|
104
|
+
subject { described_class.find(raw_token) }
|
105
|
+
|
106
|
+
it { is_expected.to be nil }
|
107
|
+
end
|
55
108
|
end
|
56
109
|
end
|
57
110
|
|
@@ -174,6 +227,13 @@ module JWTKeeper
|
|
174
227
|
it { expect(new_token).to be_valid }
|
175
228
|
it { expect(old_token.claims[:claim]).to eq new_token.claims[:claim] }
|
176
229
|
it { expect(old_token.cookie_secret).not_to eq new_token.cookie_secret }
|
230
|
+
|
231
|
+
context 'with a foreign issued token' do
|
232
|
+
let(:old_token) { described_class.create(**private_claims, iss: 'ISSUER') }
|
233
|
+
let(:new_token) { old_token.rotate }
|
234
|
+
|
235
|
+
it { expect(old_token).to eq new_token }
|
236
|
+
end
|
177
237
|
end
|
178
238
|
|
179
239
|
describe '#valid?' do
|
@@ -187,6 +247,16 @@ module JWTKeeper
|
|
187
247
|
context 'when valid' do
|
188
248
|
it { is_expected.to be_valid }
|
189
249
|
end
|
250
|
+
|
251
|
+
context 'with overriden secret' do
|
252
|
+
subject { described_class.create(**private_claims, secret: secret) }
|
253
|
+
|
254
|
+
let(:secret) { SecureRandom.uuid }
|
255
|
+
|
256
|
+
context 'when valid' do
|
257
|
+
it { is_expected.to be_valid }
|
258
|
+
end
|
259
|
+
end
|
190
260
|
end
|
191
261
|
|
192
262
|
describe '#invalid?' do
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jwt_keeper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 5.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Rivera
|
8
8
|
- Zane Wolfgang Pickett
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2021-03-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -81,6 +81,20 @@ dependencies:
|
|
81
81
|
- - ">="
|
82
82
|
- !ruby/object:Gem::Version
|
83
83
|
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: pry
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
84
98
|
- !ruby/object:Gem::Dependency
|
85
99
|
name: rspec
|
86
100
|
requirement: !ruby/object:Gem::Requirement
|
@@ -141,30 +155,30 @@ dependencies:
|
|
141
155
|
name: rails
|
142
156
|
requirement: !ruby/object:Gem::Requirement
|
143
157
|
requirements:
|
144
|
-
- - "
|
158
|
+
- - ">="
|
145
159
|
- !ruby/object:Gem::Version
|
146
|
-
version: '
|
160
|
+
version: '0'
|
147
161
|
type: :runtime
|
148
162
|
prerelease: false
|
149
163
|
version_requirements: !ruby/object:Gem::Requirement
|
150
164
|
requirements:
|
151
|
-
- - "
|
165
|
+
- - ">="
|
152
166
|
- !ruby/object:Gem::Version
|
153
|
-
version: '
|
167
|
+
version: '0'
|
154
168
|
- !ruby/object:Gem::Dependency
|
155
169
|
name: activesupport
|
156
170
|
requirement: !ruby/object:Gem::Requirement
|
157
171
|
requirements:
|
158
|
-
- - "
|
172
|
+
- - ">="
|
159
173
|
- !ruby/object:Gem::Version
|
160
|
-
version: '
|
174
|
+
version: '0'
|
161
175
|
type: :runtime
|
162
176
|
prerelease: false
|
163
177
|
version_requirements: !ruby/object:Gem::Requirement
|
164
178
|
requirements:
|
165
|
-
- - "
|
179
|
+
- - ">="
|
166
180
|
- !ruby/object:Gem::Version
|
167
|
-
version: '
|
181
|
+
version: '0'
|
168
182
|
- !ruby/object:Gem::Dependency
|
169
183
|
name: jwt
|
170
184
|
requirement: !ruby/object:Gem::Requirement
|
@@ -220,7 +234,7 @@ homepage: https://github.com/sirwolfgang/jwt_keeper
|
|
220
234
|
licenses:
|
221
235
|
- MIT
|
222
236
|
metadata: {}
|
223
|
-
post_install_message:
|
237
|
+
post_install_message:
|
224
238
|
rdoc_options: []
|
225
239
|
require_paths:
|
226
240
|
- lib
|
@@ -235,9 +249,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
235
249
|
- !ruby/object:Gem::Version
|
236
250
|
version: '0'
|
237
251
|
requirements: []
|
238
|
-
|
239
|
-
|
240
|
-
signing_key:
|
252
|
+
rubygems_version: 3.2.3
|
253
|
+
signing_key:
|
241
254
|
specification_version: 4
|
242
255
|
summary: JWT for Rails made easy
|
243
256
|
test_files:
|