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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5313f67290a724013407952cecbc2c8df761ccc32d105df14ddf377433270283
4
- data.tar.gz: a71960bcace03b25fd0bb2d58256a34138197e432a2b7fef0db88bc33264f3e1
3
+ metadata.gz: 4ce4912150544b5d9944105ef36f43022c480578eb922d6a5f001c2044a53de6
4
+ data.tar.gz: 8f1ef7fa13008c88133aac5babbf8622070db55ac3ea49eb45ef515c23e1751b
5
5
  SHA512:
6
- metadata.gz: 316164c515d750a2dafffb9c070412ec3eedaef7240748dac61e4b623e7edb7f772feffced56b9e269bc545e69fe07f58df5868cb8348c4664675c2a16650c23
7
- data.tar.gz: 1798c936fa86de719cc95915c6a32fa4c0d94b8337b6a56fe3f8cf48556d4f7ebc3224a66b712f288f0e31b12a21cf573c75a5c167df40b40cc229530cb9de9c
6
+ metadata.gz: df62c535a49f323772ee6b7fe995ada5a1906c1ac3d80916949a30c7030c058eea47105368edc97351c1853bdf11cf6f561452e21d71d2d79f32246964acf3b5
7
+ data.tar.gz: b03abb09f142d3d5b27706b9792be412ea8c5698b4c7c385a343f15cd147cf9b71202c32b24b7dbbd892a9ac054be6918b9562498f7f8a347d30d054d54ddc19
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
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
- 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
41
43
 
42
44
  def not_authenticated
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rspec/core/rake_task'
3
5
 
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', '~> 5.0'
33
- spec.add_dependency 'activesupport', '~> 5.0'
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
@@ -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,
@@ -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
 
@@ -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
@@ -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 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)
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 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
24
29
  # @return [Token] token object
25
- def self.create(private_claims)
30
+ def self.create(options)
26
31
  cookie_secret = SecureRandom.hex(16) if JWTKeeper.configuration.cookie_lock
27
- new(private_claims, cookie_secret)
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 = nil)
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(claims, cookie_secret)
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(encode, cookie_secret).nil? || revoked?
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
- 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,
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: JWTKeeper.configuration.issuer,
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
- JWTKeeper.configuration.secret.to_s + cookie_secret.to_s,
169
+ JWT.encode(claims.compact,
170
+ secret.to_s + cookie_secret.to_s,
156
171
  JWTKeeper.configuration.algorithm
157
172
  )
158
173
  end
@@ -1,4 +1,4 @@
1
1
  # Gem Version
2
2
  module JWTKeeper
3
- VERSION = '3.2.0'.freeze
3
+ VERSION = '5.0.1'.freeze
4
4
  end
@@ -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: "Jet fuel can't melt steel beams") }
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: "Jet fuel can't melt steel beams" } }
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 bad cookie' do
46
- subject { described_class.find(raw_token, 'BAD_COOKIE') }
47
- it { is_expected.to be nil }
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 'with valid cookie' do
51
- before { JWTKeeper.configure(JWTKeeper::Configuration.new(config.merge(cookie_lock: true))) }
52
- subject { described_class.find(raw_token, token.cookie_secret) }
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
@@ -1,3 +1,4 @@
1
+ require 'pry'
1
2
  require 'dotenv'
2
3
  Dotenv.load
3
4
 
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: 3.2.0
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: 2019-02-06 00:00:00.000000000 Z
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: '5.0'
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: '5.0'
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: '5.0'
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: '5.0'
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
- rubyforge_project:
239
- rubygems_version: 2.7.6
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: