jwt-auth 4.2.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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