jwt-auth 4.2.0 → 5.0.0

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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -0
  3. data/Gemfile +3 -0
  4. data/README.md +119 -18
  5. data/bin/build +22 -0
  6. data/bin/release +40 -0
  7. data/jwt-auth.gemspec +18 -15
  8. data/lib/jwt/auth.rb +2 -0
  9. data/lib/jwt/auth/access_token.rb +20 -0
  10. data/lib/jwt/auth/authenticatable.rb +16 -0
  11. data/lib/jwt/auth/authentication.rb +63 -22
  12. data/lib/jwt/auth/configuration.rb +4 -1
  13. data/lib/jwt/auth/refresh_token.rb +20 -0
  14. data/lib/jwt/auth/token.rb +49 -41
  15. data/lib/jwt/auth/version.rb +3 -1
  16. data/spec/controllers/content_controller_spec.rb +95 -0
  17. data/spec/controllers/tokens_controller_spec.rb +140 -0
  18. data/spec/dummy/Rakefile +2 -0
  19. data/spec/dummy/app/channels/application_cable/channel.rb +2 -0
  20. data/spec/dummy/app/channels/application_cable/connection.rb +2 -0
  21. data/spec/dummy/app/controllers/application_controller.rb +6 -1
  22. data/spec/dummy/app/controllers/content_controller.rb +29 -0
  23. data/spec/dummy/app/controllers/tokens_controller.rb +53 -0
  24. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  25. data/spec/dummy/app/helpers/authentication_helper.rb +2 -0
  26. data/spec/dummy/app/jobs/application_job.rb +2 -0
  27. data/spec/dummy/app/mailers/application_mailer.rb +3 -1
  28. data/spec/dummy/app/models/application_record.rb +2 -0
  29. data/spec/dummy/app/models/user.rb +3 -6
  30. data/spec/dummy/bin/bundle +2 -0
  31. data/spec/dummy/bin/rails +2 -0
  32. data/spec/dummy/bin/rake +2 -0
  33. data/spec/dummy/bin/setup +2 -0
  34. data/spec/dummy/bin/update +2 -0
  35. data/spec/dummy/bin/yarn +7 -7
  36. data/spec/dummy/config.ru +2 -0
  37. data/spec/dummy/config/application.rb +2 -0
  38. data/spec/dummy/config/boot.rb +3 -1
  39. data/spec/dummy/config/environment.rb +2 -0
  40. data/spec/dummy/config/environments/development.rb +3 -1
  41. data/spec/dummy/config/environments/production.rb +4 -2
  42. data/spec/dummy/config/environments/test.rb +2 -0
  43. data/spec/dummy/config/initializers/application_controller_renderer.rb +2 -0
  44. data/spec/dummy/config/initializers/assets.rb +2 -0
  45. data/spec/dummy/config/initializers/backtrace_silencers.rb +2 -0
  46. data/spec/dummy/config/initializers/content_security_policy.rb +2 -0
  47. data/spec/dummy/config/initializers/cookies_serializer.rb +2 -0
  48. data/spec/dummy/config/initializers/filter_parameter_logging.rb +2 -0
  49. data/spec/dummy/config/initializers/inflections.rb +2 -0
  50. data/spec/dummy/config/initializers/jwt_auth.rb +9 -2
  51. data/spec/dummy/config/initializers/mime_types.rb +2 -0
  52. data/spec/dummy/config/initializers/new_framework_defaults_5_2.rb +2 -0
  53. data/spec/dummy/config/initializers/wrap_parameters.rb +3 -1
  54. data/spec/dummy/config/puma.rb +5 -3
  55. data/spec/dummy/config/routes.rb +5 -4
  56. data/spec/dummy/config/spring.rb +4 -2
  57. data/spec/dummy/db/migrate/20170726110751_create_users.rb +2 -0
  58. data/spec/dummy/db/migrate/20170726110825_add_token_version_to_user.rb +2 -0
  59. data/spec/dummy/db/migrate/20170726112117_add_activated_to_user.rb +2 -0
  60. data/spec/dummy/db/migrate/20190221100103_add_password_to_user.rb +7 -0
  61. data/spec/dummy/db/schema.rb +10 -9
  62. data/spec/jwt/auth/access_token_spec.rb +35 -0
  63. data/spec/jwt/auth/configuration_spec.rb +36 -0
  64. data/spec/jwt/auth/refresh_token_spec.rb +35 -0
  65. data/spec/jwt/auth/token_spec.rb +144 -0
  66. data/spec/models/user_spec.rb +24 -0
  67. data/spec/rails_helper.rb +8 -0
  68. data/spec/spec_helper.rb +51 -53
  69. data/spec/support/database_cleaner.rb +22 -0
  70. data/spec/support/matchers/return_token.rb +33 -0
  71. data/version.yml +1 -0
  72. metadata +119 -54
  73. data/spec/authentication_spec.rb +0 -136
  74. data/spec/configuration_spec.rb +0 -18
  75. data/spec/dummy/app/controllers/authentication_controller.rb +0 -22
  76. data/spec/token_spec.rb +0 -125
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb9d83836fe9d226380f942eaa215d51fb3ad36d05ea5ead0aaae1fbb270931a
4
- data.tar.gz: ec4fcf71b3bea2a226e806ad26c979695fd2eaec8ee506fc078147b2145659d6
3
+ metadata.gz: c1f15acad1cbf01398773956c92432bcb1011ab1a41e37ef8829196260274b9d
4
+ data.tar.gz: add25d8fdb4425010c9a8a48fdded669818420d1e923d44a8ed7189861bf7279
5
5
  SHA512:
6
- metadata.gz: d881ce27f177aa441846d4d7cd8aa9d5518e1c543caa7a5df06f31f5a6b95d4daf13d29a2e2321e767e5ed9574908859cd055fd2458c4bb9c2bd5f44576e1334
7
- data.tar.gz: 5c94d698a8d3797416b8bb7a357fbccd203fca8fa82b92bf340fafd03e17783467b4ccdc646f66bf0aeef74153f384464c68ae667d440af37b7ad648adc3b9ce
6
+ metadata.gz: 9aaaf2d95f5a00989134c7e5be578e2078bbe8c7f2d50c4575a2fbe6acdd4b181629f0236aafe8a2a0faa20edc6b8a4b8b2e2619235fd529eb4066573b09ca62
7
+ data.tar.gz: 7f37a1e60dc8f81f61fee5d20529c62119c4f22e78b88a36fc94a8ecceff8c7a23e0b6a97d1778d85e26717f5018ccb12a946557910434cb3e6771816c80383e
@@ -1,6 +1,9 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 2.3.0
3
4
  - 2.4.0
5
+ - 2.5.0
6
+ - 2.6.0
4
7
  cache:
5
8
  bundler: true
6
9
  env:
data/Gemfile CHANGED
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  source 'https://rubygems.org'
4
+
5
+ ruby '>= 2.3'
6
+
4
7
  gemspec
data/README.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  JWT-based authentication middleware for Rails API without Devise
4
4
 
5
+ ## Concept
6
+
7
+ JWT::Auth uses a two-token authentication mechanism.
8
+ When the client authenticates against the application, a long-lived token is generated (called a refresh token).
9
+ Using this long-lived token, a short-lived token can be requested using a different endpoint.
10
+ This short-lived token (called an access token) can then be used to manipulate the API.
11
+
12
+ ```
13
+ +--------+ +---------------+
14
+ | |---- Authentication Request -->| Sign in |
15
+ | | | Endpoint |
16
+ | |<--------- Refresh Token ------| |
17
+ | | +---------------+
18
+ | |
19
+ | | +---------------+
20
+ | |--------- Refresh Token ------>| Refresh |
21
+ | Client | | Endpoint |
22
+ | |<--------- Access Token -------| |
23
+ | | +---------------+
24
+ | |
25
+ | | +---------------+
26
+ | |---------- Access Token ------>| API |
27
+ | | | Endpoint |
28
+ | |<------- Protected Resource ---| |
29
+ +--------+ +---------------+
30
+ ```
31
+
5
32
  ## Installation
6
33
 
7
34
  Add this line to your application's Gemfile:
@@ -23,9 +50,14 @@ Create an initializer:
23
50
  ```ruby
24
51
  JWT::Auth.configure do |config|
25
52
  ##
26
- # Token lifetime
53
+ # Refresh token lifetime
54
+ #
55
+ config.refresh_token_lifetime = 1.year
56
+
57
+ ##
58
+ # Access token lifetime
27
59
  #
28
- config.token_lifetime = 24.hours
60
+ config.access_token_lifetime = 2.hours
29
61
 
30
62
  ##
31
63
  # JWT secret
@@ -36,7 +68,7 @@ end
36
68
 
37
69
  Do not try to set the `model` configuration property in the initializer, as this property is already set by including the `Authenticatable` concern in your model.
38
70
 
39
- Include model methods in your user model:
71
+ Include model methods in your user model. This adds a dummy `#find_by_token` method, which you can override, and a validation for `#token_version`.
40
72
 
41
73
  ```ruby
42
74
  class User < ApplicationRecord
@@ -44,7 +76,7 @@ class User < ApplicationRecord
44
76
  end
45
77
  ```
46
78
 
47
- Optionally, define the `find_by_token` method on your model to allow additional checks (for example account activation):
79
+ Optionally, override the `#find_by_token` method on your model to allow additional checks (for example account activation):
48
80
 
49
81
  ```ruby
50
82
  def self.find_by_token(params)
@@ -52,7 +84,7 @@ def self.find_by_token(params)
52
84
  end
53
85
  ```
54
86
 
55
- Add a `token_version` field to your user model:
87
+ Generate the `token_version` migration:
56
88
 
57
89
  ```ruby
58
90
  class AddTokenVersionToUser < ActiveRecord::Migration[5.0]
@@ -71,6 +103,9 @@ class ApplicationController < ActionController::API
71
103
 
72
104
  rescue_from JWT::Auth::UnauthorizedError, :with => :handle_unauthorized
73
105
 
106
+ # Validate validity of token (if present) on all routes
107
+ before_action :validate_token
108
+
74
109
  protected
75
110
 
76
111
  def handle_unauthorized
@@ -79,24 +114,88 @@ class ApplicationController < ActionController::API
79
114
  end
80
115
  ```
81
116
 
82
- Set callbacks on routes:
117
+ Add the appropriate filters on your authentication API actions:
83
118
 
84
119
  ```ruby
85
- class MyController < ApplicationController
86
- # Authenticates user from request header
87
- # The callback raises an UnauthorizedError on missing or invalid token
88
- before_action :authenticate_user, :except => %i[create]
89
-
90
- # Validate token if there is a token present
91
- # The callback raises an UnauthorizedError only if there is a token present, and it is invalid
92
- # This prevents users from using an expired token on an unauthenticated route and getting a HTTP 2xx
93
- before_action :validate_token
94
-
95
- # Renew token and set response header
96
- after_action :renew_token
120
+ class TokensController < ApplicationController
121
+ # Validate refresh token on refresh action
122
+ before_action :validate_refresh_token, :only => :update
123
+
124
+ # Require token only on refresh action
125
+ before_action :require_token, :only => :update
126
+
127
+ ##
128
+ # POST /token
129
+ #
130
+ # Sign in the user
131
+ #
132
+ def create
133
+ @user = User.active.find_by :email => params[:email], :password => params[:password]
134
+ raise JWT::Auth::UnauthorizedError unless @user
135
+
136
+ # Return a long-lived refresh token
137
+ set_refresh_token @user
138
+
139
+ head :no_content
140
+ end
141
+
142
+ ##
143
+ #
144
+ # PATCH /token
145
+ #
146
+ # Refresh access token
147
+ #
148
+ def update
149
+ # Return a short-lived access token
150
+ set_access_token
151
+
152
+ head :no_content
153
+ end
154
+ end
155
+
156
+ ```
157
+
158
+ Set the appropriate filters on your API actions:
159
+
160
+ ```ruby
161
+ class ContentController < ApplicationController
162
+ # Validate access token on all actions
163
+ before_action :validate_access_token
164
+
165
+ # Require token for protected actions
166
+ before_action :require_token, :only => :authenticated
167
+
168
+ ##
169
+ # GET /unauthenticated
170
+ #
171
+ # This endpoint is not protected, performing a request without a token, or with a valid token will succeed
172
+ # Performing a request with an invalid token will raise an UnauthorizedError
173
+ #
174
+ def unauthenticated
175
+ head :no_content
176
+ end
177
+
178
+ ##
179
+ # GET /unauthenticated
180
+ #
181
+ # This endpoint is protected, performing a request with a valid access token will succeed
182
+ # Performing a request without a token, with an invalid token or with a refresh token will raise an UnauthorizedError
183
+ #
184
+ def authenticated
185
+ head :no_content
186
+ end
97
187
  end
98
188
  ```
99
189
 
190
+ You can find a fully working sample application in [spec/dummy](spec/dummy).
191
+
192
+ ## Migration guide
193
+
194
+ ### From 4.2 to 5.0
195
+
196
+ 5.0 includes breaking changes and introduces the concept of a refresh and an access token.
197
+ Please remove jwt-auth entirely from your application, and reinstall it using the instructions above.
198
+
100
199
  ## Contributing
101
200
 
102
201
  1. Fork it ( https://github.com/floriandejonckheere/jwt-auth/fork )
@@ -104,3 +203,5 @@ end
104
203
  3. Commit your changes (`git commit -am 'Add some feature'`)
105
204
  4. Push to the branch (`git push origin my-new-feature`)
106
205
  5. Create a new Pull Request
206
+
207
+ For your convenience, scripts to automatically increment version number and build a release were included in `bin/`.
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # build - Build and publish a gem
6
+ #
7
+
8
+ require 'yaml'
9
+
10
+ # Push to git repository
11
+ puts 'Pushing to git repository...'
12
+ `git push --follow-tags`
13
+
14
+ # Build gem
15
+ puts 'Building .gem...'
16
+ `gem build jwt-auth.gemspec`
17
+
18
+ # Publish gem
19
+ puts 'Publishing gem...'
20
+ `gem push $(ls *.gem | sort -h | tail -1)`
21
+
22
+ puts "\nRelease v#{YAML.load_file File.join __dir__, '..', 'version.yml'} published!"
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # release - Increment gem version and create a release
6
+ #
7
+
8
+ require 'semverse'
9
+ require 'yaml'
10
+
11
+ VERSION_FILE = File.join __dir__, '..', 'version.yml'
12
+
13
+ version = Semverse::Version.new YAML.load_file VERSION_FILE
14
+ new_version = nil
15
+
16
+ case ARGV.first
17
+ when '--major'
18
+ new_version = "#{version.major + 1}.0.0"
19
+ when '--minor'
20
+ new_version = "#{version.major}.#{version.minor + 1}.0"
21
+ when '--patch'
22
+ new_version = "#{version.major}.#{version.minor}.#{version.patch + 1}"
23
+ else
24
+ puts "Usage: #{__FILE__} --major | --minor | --patch"
25
+ exit! 1
26
+ end
27
+
28
+ # Write version file
29
+ File.write VERSION_FILE, new_version.to_yaml
30
+
31
+ # Create git commit
32
+ puts 'Creating release commit...'
33
+ `git add #{VERSION_FILE}`
34
+ `git commit -m 'Bump version to #{new_version}'`
35
+
36
+ # Create git tag
37
+ puts 'Creating release tag...'
38
+ `git tag v#{new_version}`
39
+
40
+ puts "\nRelease v#{new_version} created!"
@@ -1,7 +1,6 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
5
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
5
 
7
6
  require 'jwt/auth/version'
@@ -11,26 +10,30 @@ Gem::Specification.new do |gem|
11
10
  gem.version = JWT::Auth::VERSION
12
11
  gem.authors = ['Florian Dejonckheere']
13
12
  gem.email = ['florian@floriandejonckheere.be']
13
+ gem.date = Time.now.utc.strftime '%Y-%m-%d'
14
14
  gem.summary = 'JWT-based authentication for Rails API'
15
15
  gem.description = 'Authentication middleware for Rails API that uses JWTs'
16
16
  gem.homepage = 'https://github.com/floriandejonckheere/jwt-auth'
17
17
  gem.license = 'MIT'
18
18
 
19
- gem.files = `git ls-files -z`.split("\x0")
20
- gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ gem.files = `git ls-files -z`.split "\x0"
20
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename f }
21
21
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
- gem.require_paths = ['lib']
22
+ gem.require_paths = %w[lib]
23
23
 
24
- gem.add_runtime_dependency 'jwt', '~> 2.0'
25
- gem.add_runtime_dependency 'rails', '~> 5.2'
24
+ gem.add_runtime_dependency 'jwt'
25
+ gem.add_runtime_dependency 'rails'
26
26
 
27
- gem.add_development_dependency 'bundler', '~> 1.17'
28
- gem.add_development_dependency 'rubocop', '~> 0.63'
29
- gem.add_development_dependency 'rake', '~> 12.3'
30
- gem.add_development_dependency 'rspec', '~> 3.8'
31
- gem.add_development_dependency 'rspec-rails', '~> 3.8'
32
- gem.add_development_dependency 'rdoc', '~> 6.1'
33
- gem.add_development_dependency 'coveralls', '~> 0.8'
27
+ gem.add_development_dependency 'bundler'
34
28
  gem.add_development_dependency 'byebug'
35
- gem.add_development_dependency 'sqlite3'
29
+ gem.add_development_dependency 'coveralls'
30
+ gem.add_development_dependency 'database_cleaner'
31
+ gem.add_development_dependency 'rake'
32
+ gem.add_development_dependency 'rdoc'
33
+ gem.add_development_dependency 'rspec'
34
+ gem.add_development_dependency 'rspec-rails'
35
+ gem.add_development_dependency 'rubocop'
36
+ gem.add_development_dependency 'semverse'
37
+ gem.add_development_dependency 'shoulda-matchers'
38
+ gem.add_development_dependency 'sqlite3', '~> 1.3.6'
36
39
  end
@@ -8,3 +8,5 @@ require 'jwt/auth/configuration'
8
8
  require 'jwt/auth/authenticatable'
9
9
  require 'jwt/auth/authentication'
10
10
  require 'jwt/auth/token'
11
+ require 'jwt/auth/access_token'
12
+ require 'jwt/auth/refresh_token'
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt/auth/configuration'
4
+
5
+ module JWT
6
+ module Auth
7
+ ##
8
+ # JWT access token
9
+ #
10
+ class AccessToken < Token
11
+ def type
12
+ :access
13
+ end
14
+
15
+ def lifetime
16
+ JWT::Auth.access_token_lifetime
17
+ end
18
+ end
19
+ end
20
+ end
@@ -11,9 +11,25 @@ module JWT
11
11
  extend ActiveSupport::Concern
12
12
 
13
13
  included do
14
+ ##
15
+ # Define model in jwt-auth configuration
16
+ #
14
17
  JWT::Auth.configure do |config|
15
18
  config.model = name
16
19
  end
20
+
21
+ ##
22
+ # Token version validation
23
+ #
24
+ validates :token_version,
25
+ :presence => true
26
+
27
+ ##
28
+ # Dummy #find_by_token method
29
+ #
30
+ def find_by_token(*args)
31
+ find_by args
32
+ end
17
33
  end
18
34
  end
19
35
  end
@@ -9,52 +9,93 @@ module JWT
9
9
  #
10
10
  module Authentication
11
11
  ##
12
- # Current user helper
12
+ # Current user
13
13
  #
14
14
  def current_user
15
- jwt && jwt.subject
15
+ token&.subject
16
16
  end
17
17
 
18
18
  ##
19
- # Authenticate a request
19
+ # Validate a token (if it's present)
20
20
  #
21
- def authenticate_user
22
- raise JWT::Auth::UnauthorizedError unless jwt && jwt.valid?
21
+ # Apply this before_action filter for every API action
22
+ #
23
+ # @raises JWT::Auth::UnauthorizedError if a token is present and invalid
24
+ #
25
+ def validate_token
26
+ raise JWT::Auth::UnauthorizedError unless token.nil? || token&.valid?
23
27
  end
24
28
 
25
29
  ##
26
- # Validate a token (authenticate a request iff there is a token)
30
+ # Authenticate the user with the token
27
31
  #
28
- def validate_token
29
- authenticate_user if jwt
32
+ # Apply this filter for API actions that need an access token
33
+ # This filter does not enforce token presence
34
+ #
35
+ # @raises JWT::Auth::UnauthorizedError if a token is present and it is not a valid access token
36
+ #
37
+ def validate_access_token
38
+ raise JWT::Auth::UnauthorizedError unless header.nil? || token.is_a?(AccessToken)
30
39
  end
31
40
 
32
41
  ##
33
- # Add JWT header to response
42
+ # Validate a refresh token
43
+ #
44
+ # Apply this filter for the API token refresh action
45
+ # This filter does not enforce token presence
34
46
  #
35
- def renew_token
36
- return unless jwt && jwt.valid?
37
- jwt.renew!
38
- response.headers['Authorization'] = "Bearer #{jwt.to_jwt}"
47
+ # @raises JWT::Auth::UnauthorizedError if a token is present and it is not a valid refresh token
48
+ #
49
+ def validate_refresh_token
50
+ raise JWT::Auth::UnauthorizedError unless header.nil? || token.is_a?(RefreshToken)
39
51
  end
40
52
 
41
- protected
53
+ ##
54
+ # Require a token to be present
55
+ #
56
+ # Apply this filter for API actions that require an access token
57
+ #
58
+ # @raises JWT::Auth::UnauthorizedError if on token is present
59
+ #
60
+ def require_token
61
+ raise JWT::Auth::UnauthorizedError if token.nil?
62
+ end
42
63
 
43
64
  ##
44
- # Extract JWT from request
65
+ # Set API token in the response
45
66
  #
46
- def jwt
47
- return @token if @token
67
+ def set_access_token(user = current_user)
68
+ set_header JWT::Auth::AccessToken.new(:subject => user)
69
+ end
70
+
71
+ ##
72
+ # Set refresh token in the response
73
+ #
74
+ def set_refresh_token(user = current_user)
75
+ set_header JWT::Auth::RefreshToken.new(:subject => user)
76
+ end
77
+
78
+ protected
48
79
 
80
+ def token
81
+ @token ||= JWT::Auth::Token.from_jwt header
82
+ end
83
+
84
+ ##
85
+ # Extract token from request
86
+ #
87
+ def header
49
88
  header = request.env['HTTP_AUTHORIZATION']
50
89
  return nil unless header
51
90
 
52
- token = header.scan(/Bearer (.*)$/).flatten.last
53
- return nil unless token
91
+ header.scan(/Bearer (.*)$/).flatten.last
92
+ end
54
93
 
55
- @token = JWT::Auth::Token.from_token token
56
- rescue JWT::DecodeError
57
- nil
94
+ ##
95
+ # Set a token in the response
96
+ #
97
+ def set_header(token)
98
+ response.headers['Authorization'] = "Bearer #{token.to_jwt}"
58
99
  end
59
100
  end
60
101
  end