knock 1.4.2 → 1.5

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