knock 1.4.2 → 1.5

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 (48) hide show
  1. checksums.yaml +8 -8
  2. data/app/controllers/knock/application_controller.rb +1 -1
  3. data/app/controllers/knock/auth_token_controller.rb +32 -7
  4. data/app/model/knock/auth_token.rb +31 -7
  5. data/lib/generators/knock/token_controller_generator.rb +25 -0
  6. data/lib/generators/templates/entity_token_controller.rb.erb +2 -0
  7. data/lib/generators/templates/knock.rb +25 -3
  8. data/lib/knock.rb +7 -0
  9. data/lib/knock/authenticable.rb +45 -6
  10. data/lib/knock/version.rb +1 -1
  11. data/test/controllers/knock/auth_token_controller_test.rb +11 -0
  12. data/test/dummy/app/controllers/admin_protected_controller.rb +7 -0
  13. data/test/dummy/app/controllers/admin_token_controller.rb +2 -0
  14. data/test/dummy/app/controllers/composite_name_entity_protected_controller.rb +7 -0
  15. data/test/dummy/app/controllers/vendor_protected_controller.rb +11 -0
  16. data/test/dummy/app/controllers/vendor_token_controller.rb +2 -0
  17. data/test/dummy/app/models/admin.rb +16 -0
  18. data/test/dummy/app/models/composite_name_entity.rb +3 -0
  19. data/test/dummy/app/models/vendor.rb +3 -0
  20. data/test/dummy/config/initializers/knock.rb +10 -0
  21. data/test/dummy/config/routes.rb +8 -0
  22. data/test/dummy/db/migrate/20160519075733_create_admins.rb +10 -0
  23. data/test/dummy/db/migrate/20160522051816_create_vendors.rb +10 -0
  24. data/test/dummy/db/migrate/20160522181712_create_composite_name_entities.rb +10 -0
  25. data/test/dummy/db/schema.rb +22 -1
  26. data/test/dummy/db/test.sqlite3 +0 -0
  27. data/test/dummy/log/test.log +333 -91
  28. data/test/dummy/test/controllers/admin_protected_controller_test.rb +49 -0
  29. data/test/dummy/test/controllers/admin_token_controller_test.rb +22 -0
  30. data/test/dummy/test/controllers/composite_name_entity_protected_controller_test.rb +49 -0
  31. data/test/dummy/test/controllers/vendor_protected_controller_test.rb +55 -0
  32. data/test/dummy/test/controllers/vendor_token_controller_test.rb +22 -0
  33. data/test/dummy/test/models/admin_test.rb +7 -0
  34. data/test/dummy/test/models/vendor_test.rb +7 -0
  35. data/test/{dummy/test/fixtures/users.yml → fixtures/admins.yml} +1 -5
  36. data/test/fixtures/composite_name_entities.yml +5 -0
  37. data/test/fixtures/vendors.yml +5 -0
  38. data/test/generators/token_controller_generator_test.rb +31 -0
  39. data/test/model/knock/auth_token_test.rb +33 -9
  40. data/test/support/generators_test_helper.rb +9 -0
  41. data/test/test_helper.rb +9 -0
  42. data/test/tmp/app/controllers/admin_token_controller.rb +2 -0
  43. data/test/tmp/app/controllers/admin_user_token_controller.rb +2 -0
  44. data/test/tmp/app/controllers/user_admin_token_controller.rb +2 -0
  45. data/test/tmp/app/controllers/user_token_controller.rb +2 -0
  46. data/test/tmp/config/routes.rb +17 -0
  47. metadata +76 -6
  48. data/test/tmp/config/initializers/knock.rb +0 -86
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MmQ1NDdiNjFmNjkyNTEyMTIwYjlmNWNjZmU3ZDI3NjhkNTM4ZGNiNA==
4
+ MzZjZmQ3ZDIxMmQ3NDBjMTc2NmU1NGUxNmQwNWM0NmMyNzc5ZGNkYw==
5
5
  data.tar.gz: !binary |-
6
- YzlkOGFkZTZkNDRiMzFhNDA1YzQ4NGFiM2NkYTNjMzkxZTA3MjUwOQ==
6
+ YzcwZWUzNDRlYjFiOWIwMjdkZGI0NWFkY2RkZGMwMTBiYzgyYTIyMw==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- ODgxOWJmMTQxNTkzMWYzN2QyMGU0ZGZkNDkyNTE0M2RkMzM4MGVhMTQ5NjA5
10
- ZjcxOTdhNTlkNjFiNzlmZTMxYTExODg1OGI5ZjIwZmI4MjRkN2U0ZjhmODk1
11
- Y2FjMmZjMjQxYzkxZThmYTFiOTY5NzMzMTI4MmVmMDVlNWY0N2U=
9
+ YjYyMjJkZjM0ZDRmNzA4OTk4YTFiZTc3NTUzMDQzNWEwMmEyMmRjYzc1OTMw
10
+ YzMzMTZhNzgzM2ZjZDhjZGIxNTAwZjdkMGEwZDIyZjVkMmYzZDI1ZWMxMWZj
11
+ YTQ3MTE3OWY5YWRiODk1ZDkwMjZlZTkzM2ZjYzA5MjdhNzU5NjA=
12
12
  data.tar.gz: !binary |-
13
- ZWY5ZTczMGJmOTkwYThmZmIzMzgzMWUxZDNmZDU3YjJkY2RlODk0NDJmNWY4
14
- ZDM4NTI1MDQ3ZjFlYjZlYmUyZjViMjllNDE4NWE4OWFiZGM3Y2NhNGM0ZGYx
15
- NjFjZTZkNjU4ZTViNGUxNmNiNWM2NjJjOGU3NjRkMDU5NWYzMmM=
13
+ NDM1NGRjNjlkYWM2NWY3YTVhZTNkNTFiNGYyY2M3NjdhMmVlNmJlMDE0MTc1
14
+ ZTJhY2I0M2RlNTY4NDEzYzIzZGI2Mjk4NTkyNGVmODljOWYwNWNmMDYzNjgz
15
+ OTFmMmU5OGY3MjQ3MTFlNWNlMDQyMDRlMWJhNzIxOTJlYjlmZWM=
@@ -1,6 +1,6 @@
1
1
  module Knock
2
2
  class ApplicationController < ActionController::Base
3
- rescue_from ActiveRecord::RecordNotFound, with: :not_found
3
+ rescue_from Knock.not_found_exception_class_name, with: :not_found
4
4
 
5
5
  private
6
6
 
@@ -2,23 +2,48 @@ require_dependency "knock/application_controller"
2
2
 
3
3
  module Knock
4
4
  class AuthTokenController < ApplicationController
5
- before_action :authenticate!
5
+ before_action :authenticate
6
6
 
7
7
  def create
8
- render json: { jwt: auth_token.token }, status: :created
8
+ render json: auth_token, status: :created
9
9
  end
10
10
 
11
11
  private
12
- def authenticate!
13
- raise ActiveRecord::RecordNotFound unless user.authenticate(auth_params[:password])
12
+ def authenticate
13
+ unless entity.present? && entity.authenticate(auth_params[:password])
14
+ raise Knock.not_found_exception_class
15
+ end
14
16
  end
15
17
 
16
18
  def auth_token
17
- AuthToken.new payload: { sub: user.id }
19
+ if entity.respond_to? :to_token_payload
20
+ AuthToken.new payload: entity.to_token_payload
21
+ else
22
+ AuthToken.new payload: { sub: entity.id }
23
+ end
18
24
  end
19
25
 
20
- def user
21
- Knock.current_user_from_handle.call auth_params[Knock.handle_attr]
26
+ def entity
27
+ @entity ||=
28
+ if self.class.name == "Knock::AuthTokenController"
29
+ warn "[DEPRECATION]: Routing to `AuthTokenController` directly is deprecated. Please use `<Entity Name>TokenController` inheriting from it instead. E.g. `UserTokenController`"
30
+ warn "[DEPRECATION]: Relying on `Knock.current_user_from_handle` is deprecated. Please implement `User#from_token_request` instead."
31
+ Knock.current_user_from_handle.call auth_params[Knock.handle_attr]
32
+ else
33
+ if entity_class.respond_to? :from_token_request
34
+ entity_class.from_token_request request
35
+ else
36
+ entity_class.find_by email: auth_params[:email]
37
+ end
38
+ end
39
+ end
40
+
41
+ def entity_class
42
+ entity_name.constantize
43
+ end
44
+
45
+ def entity_name
46
+ self.class.name.split('TokenController').first
22
47
  end
23
48
 
24
49
  def auth_params
@@ -3,6 +3,7 @@ require 'jwt'
3
3
  module Knock
4
4
  class AuthToken
5
5
  attr_reader :token
6
+ attr_reader :payload
6
7
 
7
8
  def initialize payload: {}, token: nil
8
9
  if token.present?
@@ -16,8 +17,21 @@ module Knock
16
17
  end
17
18
  end
18
19
 
19
- def current_user
20
- @current_user ||= Knock.current_user_from_token.call @payload
20
+ def entity_for entity_class
21
+ if entity_class.respond_to? :from_token_payload
22
+ entity_class.from_token_payload @payload
23
+ else
24
+ if entity_class.to_s == "User" && Knock.respond_to?(:current_user_from_token)
25
+ warn "[DEPRECATION]: `Knock.current_user_from_token` is deprecated. Please implement `User.from_token_payload` instead."
26
+ Knock.current_user_from_token.call @payload
27
+ else
28
+ entity_class.find @payload['sub']
29
+ end
30
+ end
31
+ end
32
+
33
+ def to_json options = {}
34
+ {jwt: @token}.to_json
21
35
  end
22
36
 
23
37
  private
@@ -36,15 +50,25 @@ module Knock
36
50
  end
37
51
 
38
52
  def claims
39
- {
40
- exp: Knock.token_lifetime.from_now.to_i,
41
- aud: token_audience
42
- }
53
+ _claims = {}
54
+ _claims[:exp] = token_lifetime if verify_lifetime?
55
+ _claims[:aud] = token_audience if verify_audience?
56
+ _claims
57
+ end
58
+
59
+ def token_lifetime
60
+ Knock.token_lifetime.from_now.to_i if verify_lifetime?
61
+ end
62
+
63
+ def verify_lifetime?
64
+ !Knock.token_lifetime.nil?
43
65
  end
44
66
 
45
67
  def verify_claims
46
68
  {
47
- aud: token_audience, verify_aud: verify_audience?
69
+ aud: token_audience,
70
+ verify_aud: verify_audience?,
71
+ verify_expiration: verify_lifetime?
48
72
  }
49
73
  end
50
74
 
@@ -0,0 +1,25 @@
1
+ module Knock
2
+ class TokenControllerGenerator < Rails::Generators::Base
3
+ source_root File.expand_path("../../templates", __FILE__)
4
+ argument :name, type: :string
5
+
6
+ desc <<-DESC
7
+ Creates a Knock token controller for the given entity
8
+ and add the corresponding routes.
9
+ DESC
10
+
11
+ def copy_controller_file
12
+ template 'entity_token_controller.rb.erb', "app/controllers/#{name.underscore}_token_controller.rb"
13
+ end
14
+
15
+ def add_route
16
+ route "post '#{name.underscore}_token' => '#{name.underscore}_token#create'"
17
+ end
18
+
19
+ private
20
+
21
+ def entity_name
22
+ name
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,2 @@
1
+ class <%= entity_name.camelize %>TokenController < Knock::AuthTokenController
2
+ end
@@ -1,5 +1,8 @@
1
1
  Knock.setup do |config|
2
2
 
3
+ ## [DEPRECATED]
4
+ ## This is deprecated in favor of `User.from_token_request`.
5
+ ##
3
6
  ## User handle attribute
4
7
  ## ---------------------
5
8
  ##
@@ -8,6 +11,9 @@ Knock.setup do |config|
8
11
  ## Default:
9
12
  # config.handle_attr = :email
10
13
 
14
+ ## [DEPRECATED]
15
+ ## This is deprecated in favor of `User.from_token_request`.
16
+ ##
11
17
  ## Current user retrieval from handle when signing in
12
18
  ## --------------------------------------------------
13
19
  ##
@@ -18,11 +24,16 @@ Knock.setup do |config|
18
24
  ## AuthTokenController parameters. It also uses the same variable to enforce
19
25
  ## permitted values in the controller.
20
26
  ##
21
- ## You must raise ActiveRecord::RecordNotFound if the resource cannot be retrieved.
27
+ ## You must raise an exception if the resource cannot be retrieved.
28
+ ## The type of the exception is configured in config.not_found_exception_class_name,
29
+ ## and it is ActiveRecord::RecordNotFound by default
22
30
  ##
23
31
  ## Default:
24
32
  # config.current_user_from_handle = -> (handle) { User.find_by! Knock.handle_attr => handle }
25
33
 
34
+ ## [DEPRECATED]
35
+ ## This is depreacted in favor of `User.from_token_payload`.
36
+ ##
26
37
  ## Current user retrieval when validating token
27
38
  ## --------------------------------------------
28
39
  ##
@@ -30,7 +41,9 @@ Knock.setup do |config|
30
41
  ## By default, it assumes you have a model called `User` and that
31
42
  ## the user_id is stored in the 'sub' claim.
32
43
  ##
33
- ## You must raise ActiveRecord::RecordNotFound if the resource cannot be retrieved.
44
+ ## You must raise an exception if the resource cannot be retrieved.
45
+ ## The type of the exception is configured in config.not_found_exception_class_name,
46
+ ## and it is ActiveRecord::RecordNotFound by default
34
47
  ##
35
48
  ## Default:
36
49
  # config.current_user_from_token = -> (claims) { User.find claims['sub'] }
@@ -39,7 +52,8 @@ Knock.setup do |config|
39
52
  ## Expiration claim
40
53
  ## ----------------
41
54
  ##
42
- ## How long before a token is expired.
55
+ ## How long before a token is expired. If nil is provided, token will
56
+ ## last forever.
43
57
  ##
44
58
  ## Default:
45
59
  # config.token_lifetime = 1.day
@@ -83,4 +97,12 @@ Knock.setup do |config|
83
97
  ##
84
98
  ## Default:
85
99
  # config.token_public_key = nil
100
+
101
+ ## Exception Class
102
+ ## ---------------
103
+ ##
104
+ ## Configure the exception to be used when user cannot be found.
105
+ ##
106
+ ## Default:
107
+ # config.not_found_exception_class_name = 'ActiveRecord::RecordNotFound'
86
108
  end
@@ -26,6 +26,13 @@ module Knock
26
26
  mattr_accessor :token_public_key
27
27
  self.token_public_key = nil
28
28
 
29
+ mattr_accessor :not_found_exception_class_name
30
+ self.not_found_exception_class_name = 'ActiveRecord::RecordNotFound'
31
+
32
+ def self.not_found_exception_class
33
+ not_found_exception_class_name.to_s.constantize
34
+ end
35
+
29
36
  # Default way to setup Knock. Run `rails generate knock:install` to create
30
37
  # a fresh initializer with all configuration values.
31
38
  def self.setup
@@ -1,14 +1,53 @@
1
1
  module Knock::Authenticable
2
- def current_user
3
- @current_user ||= begin
4
- token = params[:token] || request.headers['Authorization'].split.last
5
- Knock::AuthToken.new(token: token).current_user
2
+ def authenticate
3
+ warn "[DEPRECATION]: `authenticate` is deprecated. Please use `authenticate_user` instead."
4
+ head(:unauthorized) unless authenticate_for(User)
5
+ end
6
+
7
+ def authenticate_for entity_class
8
+ token = params[:token] || token_from_request_headers
9
+ return nil if token.nil?
10
+
11
+ begin
12
+ @entity = Knock::AuthToken.new(token: token).entity_for(entity_class)
13
+ define_current_entity_getter(entity_class)
14
+ @entity
6
15
  rescue
7
16
  nil
8
17
  end
9
18
  end
10
19
 
11
- def authenticate
12
- head :unauthorized unless current_user
20
+ private
21
+
22
+ def method_missing(method, *args)
23
+ prefix, entity_name = method.to_s.split('_', 2)
24
+ case prefix
25
+ when 'authenticate'
26
+ head(:unauthorized) unless authenticate_entity(entity_name)
27
+ when 'current'
28
+ authenticate_entity(entity_name)
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ def authenticate_entity(entity_name)
35
+ entity_class = entity_name.camelize.constantize
36
+ send(:authenticate_for, entity_class)
37
+ end
38
+
39
+ def token_from_request_headers
40
+ unless request.headers['Authorization'].nil?
41
+ request.headers['Authorization'].split.last
42
+ end
43
+ end
44
+
45
+ def define_current_entity_getter entity_class
46
+ getter_name = "current_#{entity_class.to_s.underscore}"
47
+ unless self.respond_to?(getter_name)
48
+ self.class.send(:define_method, getter_name) do
49
+ @entity ||= nil
50
+ end
51
+ end
13
52
  end
14
53
  end
@@ -1,3 +1,3 @@
1
1
  module Knock
2
- VERSION = "1.4.2"
2
+ VERSION = "1.5"
3
3
  end
@@ -10,6 +10,10 @@ module Knock
10
10
  @user ||= users(:one)
11
11
  end
12
12
 
13
+ test "it's using configured custom exception" do
14
+ assert_equal Knock.not_found_exception_class, Knock::MyCustomException
15
+ end
16
+
13
17
  test "responds with 404 if user does not exist" do
14
18
  post :create, auth: { email: 'wrong@example.net', password: '' }
15
19
  assert_response :not_found
@@ -24,5 +28,12 @@ module Knock
24
28
  post :create, auth: { email: user.email, password: 'secret' }
25
29
  assert_response :created
26
30
  end
31
+
32
+ test "response contains token" do
33
+ post :create, auth: { email: user.email, password: 'secret' }
34
+
35
+ content = JSON.parse(response.body)
36
+ assert_equal true, content.has_key?("jwt")
37
+ end
27
38
  end
28
39
  end
@@ -0,0 +1,7 @@
1
+ class AdminProtectedController < ApplicationController
2
+ before_action :authenticate_admin
3
+
4
+ def index
5
+ head :ok
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ class AdminTokenController < Knock::AuthTokenController
2
+ end
@@ -0,0 +1,7 @@
1
+ class CompositeNameEntityProtectedController < ApplicationController
2
+ before_action :authenticate_composite_name_entity
3
+
4
+ def index
5
+ head :ok
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ class VendorProtectedController < ApplicationController
2
+ before_action :authenticate_vendor, only: [:index]
3
+ before_action :some_missing_method, only: [:show]
4
+
5
+ def index
6
+ head :ok
7
+ end
8
+
9
+ def show
10
+ end
11
+ end
@@ -0,0 +1,2 @@
1
+ class VendorTokenController < Knock::AuthTokenController
2
+ end
@@ -0,0 +1,16 @@
1
+ class Admin < ActiveRecord::Base
2
+ has_secure_password
3
+
4
+ def self.from_token_request request
5
+ email = request.params["auth"] && request.params["auth"]["email"]
6
+ self.find_by email: email
7
+ end
8
+
9
+ def self.from_token_payload payload
10
+ self.find payload["sub"]
11
+ end
12
+
13
+ def to_token_payload
14
+ {sub: id}
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ class CompositeNameEntity < ActiveRecord::Base
2
+ has_secure_password
3
+ end
@@ -0,0 +1,3 @@
1
+ class Vendor < ActiveRecord::Base
2
+ has_secure_password
3
+ end
@@ -0,0 +1,10 @@
1
+ Knock.setup do |config|
2
+ config.token_signature_algorithm = 'HS256'
3
+ config.token_secret_signature_key = -> { Rails.application.secrets.secret_key_base }
4
+ config.token_public_key = nil
5
+ config.token_audience = nil
6
+
7
+ config.current_user_from_handle = -> handle { User.find_by(Knock.handle_attr => handle) || raise(Knock::MyCustomException) }
8
+ config.current_user_from_token = -> claims { User.find_by(id: claims['sub']) || raise(Knock::MyCustomException) }
9
+ config.not_found_exception_class_name = 'Knock::MyCustomException'
10
+ end
@@ -1,5 +1,13 @@
1
1
  Rails.application.routes.draw do
2
+ post 'admin_token' => 'admin_token#create'
3
+ post 'vendor_token' => 'vendor_token#create'
4
+
2
5
  resources :protected_resources
3
6
  resource :current_user
7
+
8
+ resources :admin_protected
9
+ resources :composite_name_entity_protected
10
+ resources :vendor_protected
11
+
4
12
  mount Knock::Engine => "/knock"
5
13
  end