jwt_keeper 3.3.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2c902b7848d7fe72b7ad437badc5cf549b299d4b2ae2dc9150b7f42d63855a3
4
- data.tar.gz: 77a18a0cfeb2b198f13affa3792feaeb1235d5fa2f6ae599038bb3e2245b41b2
3
+ metadata.gz: 135ae5f5451cdcd997de5a5208642833ef5d043038b020fc96b67415c243b8b7
4
+ data.tar.gz: f940045097cb8cbd2d281bd1b887598f7c2f0f7bf25e598b834978e61a9337f8
5
5
  SHA512:
6
- metadata.gz: bc177da58d088bdaeeba97049628d0908f05ef9bd717674a34b511a14e70289604a87647c01853ec3aeb988fb80cd95f4c4a96d0741a1133efc72f2138b9bd98
7
- data.tar.gz: 97dde3b11a5584357028abb7d9523d548c53ccb8e0211a4b31089f177747c1d9fca7521bf4984837a540ec2c46080a51adacd30e322fcd4408e6d8d732cad21e
6
+ metadata.gz: dfa8fe29790a114e9f180822f32015fb5881cc268b249b757bec44df0ed419b203e33c27f439ed019cbdeecdbc6aa34512930c4210b5c000a77adb5da6dee07c
7
+ data.tar.gz: 881df471eaa098d00614578f551d2e67414cdee8260c45e7ad90a2cdca71fd2af52b3c88ebd8d9fa61bedaf9863dceaee7281a6f5f92f56b238111bf4b382527
data/README.md CHANGED
@@ -32,12 +32,15 @@ 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
- rake generate jwt_keeper:install
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
43
+ rescue_from JWTKeeper::NotAuthenticatedError, with: :not_authenticated
41
44
 
42
45
  def not_authenticated
43
46
  # Overload to return status 401
@@ -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
@@ -23,6 +23,4 @@ module JWTKeeper
23
23
 
24
24
  @configuration = new_configuration.freeze
25
25
  end
26
-
27
- require 'jwt_keeper/engine' if defined?(Rails)
28
26
  end
@@ -4,8 +4,8 @@ module JWTKeeper
4
4
  algorithm: 'HS512',
5
5
  secret: nil,
6
6
  expiry: 24.hours,
7
- issuer: 'api.example.com',
8
- audience: 'example.com',
7
+ issuer: nil,
8
+ audience: nil,
9
9
  redis_connection: nil,
10
10
  version: nil,
11
11
  cookie_lock: false,
@@ -10,7 +10,7 @@ module JWTKeeper
10
10
 
11
11
  if token.nil?
12
12
  clear_authentication_token
13
- return not_authenticated
13
+ raise JWTKeeper::NotAuthenticatedError
14
14
  end
15
15
 
16
16
  if token.version_mismatch? || token.pending?
@@ -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,17 +47,10 @@ 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
 
54
- # The default action for denying non-authenticated connections.
55
- # You can override this method in your controllers
56
- # @return [void]
57
- def not_authenticated
58
- redirect_to root_path
59
- end
60
-
61
54
  # The default action for accepting authenticated connections.
62
55
  # You can override this method in your controllers
63
56
  # @return [void]
@@ -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.setex(jti, seconds, type)
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.get(jti)
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
@@ -1,4 +1,7 @@
1
1
  module JWTKeeper
2
+ # the session is invalid
3
+ class NotAuthenticatedError < StandardError; end
4
+
2
5
  # The token is invalid
3
6
  class InvalidTokenError < StandardError; end
4
7
 
@@ -2,41 +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 private_claims [Hash] the custom claims to encode
9
- # @param cookie_secret [String] the cookie secret to use during encoding
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(private_claims = {}, cookie_secret = nil)
12
- @cookie_secret = cookie_secret
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!(private_claims)
22
+ @claims.merge!(options)
20
23
  @claims[:exp] = @claims[:exp].to_i if @claims[:exp].is_a?(Time)
21
24
  end
22
25
 
23
26
  # Creates a new web token
24
- # @param private_claims [Hash] the custom claims to encode
27
+ # @param options [Hash] the custom claims to encode
28
+ # @param secret the secret to use during encoding, defaults to config
25
29
  # @return [Token] token object
26
- def self.create(private_claims)
30
+ def self.create(options)
27
31
  cookie_secret = SecureRandom.hex(16) if JWTKeeper.configuration.cookie_lock
28
- new(private_claims, cookie_secret)
32
+ new(options.merge(cookie_secret: cookie_secret))
29
33
  end
30
34
 
31
35
  # Decodes and validates an existing token
32
36
  # @param raw_token [String] the raw token
33
37
  # @param cookie_secret [String] the cookie secret
34
38
  # @return [Token] token object
35
- def self.find(raw_token, cookie_secret = nil)
36
- 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)
37
41
  return nil if claims.nil?
38
42
 
39
- new_token = new(claims, cookie_secret)
43
+ new_token = new(secret: secret, cookie_secret: cookie_secret, iss: iss)
44
+ new_token.claims = claims
45
+
40
46
  return nil if new_token.revoked?
41
47
  new_token
42
48
  end
@@ -67,6 +73,7 @@ module JWTKeeper
67
73
  # @param new_claims [Hash] Used to override and update claims during rotation
68
74
  # @return [Token]
69
75
  def rotate(new_claims = nil)
76
+ return self if claims[:iss] != JWTKeeper.configuration.issuer
70
77
  revoke
71
78
 
72
79
  new_claims ||= claims.except(:iss, :aud, :exp, :nbf, :iat, :jti)
@@ -111,7 +118,11 @@ module JWTKeeper
111
118
  # Checks if the token invalid?
112
119
  # @return [Boolean]
113
120
  def invalid?
114
- self.class.decode(encode, cookie_secret).nil? || revoked?
121
+ self.class.decode(
122
+ encode,
123
+ secret: secret,
124
+ cookie_secret: cookie_secret
125
+ ).nil? || revoked?
115
126
  end
116
127
 
117
128
  # Encodes the jwt
@@ -131,8 +142,11 @@ module JWTKeeper
131
142
  end
132
143
 
133
144
  # @!visibility private
134
- def self.decode(raw_token, cookie_secret)
135
- JWT.decode(raw_token, JWTKeeper.configuration.secret.to_s + cookie_secret.to_s, true,
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,
136
150
  algorithm: JWTKeeper.configuration.algorithm,
137
151
  verify_iss: true,
138
152
  verify_aud: true,
@@ -140,7 +154,7 @@ module JWTKeeper
140
154
  verify_sub: false,
141
155
  verify_jti: false,
142
156
  leeway: 0,
143
- iss: JWTKeeper.configuration.issuer,
157
+ iss: iss,
144
158
  aud: JWTKeeper.configuration.audience
145
159
  ).first.symbolize_keys
146
160
 
@@ -152,8 +166,8 @@ module JWTKeeper
152
166
 
153
167
  # @!visibility private
154
168
  def encode
155
- JWT.encode(claims,
156
- JWTKeeper.configuration.secret.to_s + cookie_secret.to_s,
169
+ JWT.encode(claims.compact,
170
+ secret.to_s + cookie_secret.to_s,
157
171
  JWTKeeper.configuration.algorithm
158
172
  )
159
173
  end
@@ -1,4 +1,4 @@
1
1
  # Gem Version
2
2
  module JWTKeeper
3
- VERSION = '3.3.0'.freeze
3
+ VERSION = '6.0.0'.freeze
4
4
  end
@@ -5,6 +5,7 @@ RSpec.describe JWTKeeper do
5
5
  include_context 'initialize config'
6
6
 
7
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
@@ -51,7 +52,6 @@ RSpec.describe JWTKeeper do
51
52
  it { is_expected.to respond_to(:read_authentication_token) }
52
53
  it { is_expected.to respond_to(:write_authentication_token) }
53
54
  it { is_expected.to respond_to(:clear_authentication_token) }
54
- it { is_expected.to respond_to(:not_authenticated) }
55
55
  it { is_expected.to respond_to(:authenticated) }
56
56
  it { is_expected.to respond_to(:regenerate_claims) }
57
57
  end
@@ -76,13 +76,9 @@ RSpec.describe JWTKeeper do
76
76
 
77
77
  context 'with expired token' do
78
78
  let(:token) { JWTKeeper::Token.create(exp: 3.hours.ago) }
79
- before do
80
- allow(test_controller).to receive(:not_authenticated)
81
- end
82
79
 
83
- it 'calls not_authenticated' do
84
- subject.require_authentication
85
- expect(subject).to have_received(:not_authenticated).once
80
+ it 'raises NotAuthenticated' do
81
+ expect { subject.require_authentication }.to raise_error(JWTKeeper::NotAuthenticatedError)
86
82
  end
87
83
  end
88
84
 
@@ -161,16 +157,5 @@ RSpec.describe JWTKeeper do
161
157
  expect(subject.response.headers['Authorization']).to be_nil
162
158
  end
163
159
  end
164
-
165
- describe '#not_authenticated' do
166
- before do
167
- allow(test_controller).to receive(:redirect_to)
168
- end
169
-
170
- it 'it calls redirect_to' do
171
- subject.not_authenticated
172
- expect(subject).to have_received(:redirect_to).with('/')
173
- end
174
- end
175
160
  end
176
161
  end
@@ -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
@@ -46,20 +65,47 @@ module JWTKeeper
46
65
  before { JWTKeeper.configure(JWTKeeper::Configuration.new(config.merge(cookie_lock: true))) }
47
66
 
48
67
  context 'with no cookie' do
49
- subject { described_class.find(raw_token, nil) }
68
+ subject { described_class.find(raw_token, cookie_secret: nil) }
50
69
  it { is_expected.to be nil }
51
70
  end
52
71
 
53
72
  context 'with bad cookie' do
54
- subject { described_class.find(raw_token, 'BAD_COOKIE') }
73
+ subject { described_class.find(raw_token, cookie_secret: 'BAD_COOKIE') }
55
74
  it { is_expected.to be nil }
56
75
  end
57
76
 
58
77
  context 'with valid cookie' do
59
- subject { described_class.find(raw_token, token.cookie_secret) }
78
+ subject { described_class.find(raw_token, cookie_secret: token.cookie_secret) }
60
79
  it { is_expected.to be_instance_of described_class }
61
80
  end
62
81
  end
82
+
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 }
88
+
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
108
+ end
63
109
  end
64
110
 
65
111
  describe '.rotate' do
@@ -181,6 +227,13 @@ module JWTKeeper
181
227
  it { expect(new_token).to be_valid }
182
228
  it { expect(old_token.claims[:claim]).to eq new_token.claims[:claim] }
183
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
184
237
  end
185
238
 
186
239
  describe '#valid?' do
@@ -194,6 +247,16 @@ module JWTKeeper
194
247
  context 'when valid' do
195
248
  it { is_expected.to be_valid }
196
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
197
260
  end
198
261
 
199
262
  describe '#invalid?' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt_keeper
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Rivera
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-12-11 00:00:00.000000000 Z
12
+ date: 2021-03-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -249,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
249
249
  - !ruby/object:Gem::Version
250
250
  version: '0'
251
251
  requirements: []
252
- rubygems_version: 3.1.4
252
+ rubygems_version: 3.2.3
253
253
  signing_key:
254
254
  specification_version: 4
255
255
  summary: JWT for Rails made easy