api_guard 0.2.1 → 0.5.1

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 (34) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +95 -15
  3. data/Rakefile +2 -5
  4. data/app/controllers/api_guard/application_controller.rb +2 -0
  5. data/app/controllers/api_guard/authentication_controller.rb +6 -4
  6. data/app/controllers/api_guard/passwords_controller.rb +4 -2
  7. data/app/controllers/api_guard/registration_controller.rb +4 -2
  8. data/app/controllers/api_guard/tokens_controller.rb +5 -3
  9. data/config/locales/en.yml +22 -0
  10. data/config/routes.rb +2 -0
  11. data/lib/api_guard.rb +11 -6
  12. data/lib/api_guard/app_secret_key.rb +22 -0
  13. data/lib/api_guard/engine.rb +4 -5
  14. data/lib/api_guard/jwt_auth/authentication.rb +34 -12
  15. data/lib/api_guard/jwt_auth/blacklist_token.rb +7 -3
  16. data/lib/api_guard/jwt_auth/json_web_token.rb +11 -5
  17. data/lib/api_guard/jwt_auth/refresh_jwt_token.rb +4 -0
  18. data/lib/api_guard/models/concerns.rb +8 -6
  19. data/lib/api_guard/modules.rb +13 -11
  20. data/lib/api_guard/resource_mapper.rb +3 -1
  21. data/lib/api_guard/response_formatters/renderer.rb +5 -2
  22. data/lib/api_guard/route_mapper.rb +58 -54
  23. data/lib/api_guard/test/controller_helper.rb +2 -0
  24. data/lib/api_guard/version.rb +3 -1
  25. data/lib/generators/api_guard/controllers/controllers_generator.rb +9 -7
  26. data/lib/generators/api_guard/controllers/templates/authentication_controller.rb +4 -4
  27. data/lib/generators/api_guard/controllers/templates/passwords_controller.rb +3 -3
  28. data/lib/generators/api_guard/controllers/templates/registration_controller.rb +3 -3
  29. data/lib/generators/api_guard/controllers/templates/tokens_controller.rb +7 -4
  30. data/lib/generators/api_guard/initializer/initializer_generator.rb +3 -1
  31. data/lib/generators/api_guard/initializer/templates/initializer.rb +6 -4
  32. metadata +54 -69
  33. data/app/models/api_guard/application_record.rb +0 -5
  34. data/app/views/layouts/api_guard/application.html.erb +0 -14
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiGuard
2
4
  module JwtAuth
3
5
  # Common module for API authentication
@@ -14,22 +16,26 @@ module ApiGuard
14
16
  end
15
17
  end
16
18
 
19
+ def respond_to_missing?(method_name, include_private = false)
20
+ method_name.to_s.start_with?('authenticate_and_set_') || super
21
+ end
22
+
17
23
  # Authenticate the JWT token and set resource
18
24
  def authenticate_and_set_resource(resource_name)
19
25
  @resource_name = resource_name
20
26
 
21
27
  @token = request.headers['Authorization']&.split('Bearer ')&.last
22
- return render_error(401, message: 'Access token is missing in the request') unless @token
28
+ return render_error(401, message: I18n.t('api_guard.access_token.missing')) unless @token
23
29
 
24
30
  authenticate_token
25
31
 
26
32
  # Render error response only if no resource found and no previous render happened
27
- render_error(401, message: 'Invalid access token') if !current_resource && !performed?
33
+ render_error(401, message: I18n.t('api_guard.access_token.invalid')) if !current_resource && !performed?
28
34
  rescue JWT::DecodeError => e
29
35
  if e.message == 'Signature has expired'
30
- render_error(401, message: 'Access token expired')
36
+ render_error(401, message: I18n.t('api_guard.access_token.expired'))
31
37
  else
32
- render_error(401, message: 'Invalid access token')
38
+ render_error(401, message: I18n.t('api_guard.access_token.invalid'))
33
39
  end
34
40
  end
35
41
 
@@ -43,9 +49,19 @@ module ApiGuard
43
49
 
44
50
  # Returns whether the JWT token is issued after the last password change
45
51
  # Returns true if password hasn't changed by the user
46
- def valid_issued_at?
52
+ def valid_issued_at?(resource)
47
53
  return true unless ApiGuard.invalidate_old_tokens_on_password_change
48
- !current_resource.token_issued_at || @decoded_token[:iat] >= current_resource.token_issued_at.to_i
54
+
55
+ !resource.token_issued_at || @decoded_token[:iat] >= resource.token_issued_at.to_i
56
+ end
57
+
58
+ # Defines "current_{{resource_name}}" method and "@current_{{resource_name}}" instance variable
59
+ # that returns "resource" value
60
+ def define_current_resource_accessors(resource)
61
+ define_singleton_method("current_#{@resource_name}") do
62
+ instance_variable_get("@current_#{@resource_name}") ||
63
+ instance_variable_set("@current_#{@resource_name}", resource)
64
+ end
49
65
  end
50
66
 
51
67
  # Authenticate the resource with the '{{resource_name}}_id' in the decoded JWT token
@@ -54,21 +70,27 @@ module ApiGuard
54
70
  # Also, set "current_{{resource_name}}" method and "@current_{{resource_name}}" instance variable
55
71
  # for accessing the authenticated resource
56
72
  def authenticate_token
57
- return unless decode_token && @decoded_token[:"#{@resource_name}_id"].present?
73
+ return unless decode_token
58
74
 
59
- resource = @resource_name.classify.constantize.find_by(id: @decoded_token[:"#{@resource_name}_id"])
75
+ resource = find_resource_from_token(@resource_name.classify.constantize)
60
76
 
61
- self.class.send(:define_method, "current_#{@resource_name}") do
62
- instance_variable_get("@current_#{@resource_name}") || instance_variable_set("@current_#{@resource_name}", resource)
77
+ if resource && valid_issued_at?(resource) && !blacklisted?(resource)
78
+ define_current_resource_accessors(resource)
79
+ else
80
+ render_error(401, message: I18n.t('api_guard.access_token.invalid'))
63
81
  end
82
+ end
64
83
 
65
- return if current_resource && valid_issued_at? && !blacklisted?
84
+ def find_resource_from_token(resource_class)
85
+ resource_id = @decoded_token[:"#{@resource_name}_id"]
86
+ return if resource_id.blank?
66
87
 
67
- render_error(401, message: 'Invalid access token')
88
+ resource_class.find_by(id: resource_id)
68
89
  end
69
90
 
70
91
  def current_resource
71
92
  return unless respond_to?("current_#{@resource_name}")
93
+
72
94
  public_send("current_#{@resource_name}")
73
95
  end
74
96
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiGuard
2
4
  module JwtAuth
3
5
  # Common module for token blacklisting functionality
@@ -16,14 +18,16 @@ module ApiGuard
16
18
  end
17
19
 
18
20
  # Returns whether the JWT token is blacklisted or not
19
- def blacklisted?
20
- return false unless token_blacklisting_enabled?(current_resource)
21
- blacklisted_tokens_for(current_resource).exists?(token: @token)
21
+ def blacklisted?(resource)
22
+ return false unless token_blacklisting_enabled?(resource)
23
+
24
+ blacklisted_tokens_for(resource).exists?(token: @token)
22
25
  end
23
26
 
24
27
  # Blacklist the current JWT token from future access
25
28
  def blacklist_token
26
29
  return unless token_blacklisting_enabled?(current_resource)
30
+
27
31
  blacklisted_tokens_for(current_resource).create(token: @token, expire_at: Time.at(@decoded_token[:exp]).utc)
28
32
  end
29
33
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'jwt'
2
4
 
3
5
  module ApiGuard
@@ -9,11 +11,11 @@ module ApiGuard
9
11
  end
10
12
 
11
13
  def token_expire_at
12
- @expire_at ||= (current_time + ApiGuard.token_validity).to_i
14
+ @token_expire_at ||= (current_time + ApiGuard.token_validity).to_i
13
15
  end
14
16
 
15
17
  def token_issued_at
16
- @issued_at ||= current_time.to_i
18
+ @token_issued_at ||= current_time.to_i
17
19
  end
18
20
 
19
21
  # Encode the payload with the secret key and return the JWT token
@@ -33,13 +35,16 @@ module ApiGuard
33
35
  #
34
36
  # This creates expired JWT token if the argument 'expired_token' is true which can be used for testing.
35
37
  def jwt_and_refresh_token(resource, resource_name, expired_token = false)
36
- access_token = encode(
38
+ payload = {
37
39
  "#{resource_name}_id": resource.id,
38
40
  exp: expired_token ? token_issued_at : token_expire_at,
39
41
  iat: token_issued_at
40
- )
42
+ }
41
43
 
42
- [access_token, new_refresh_token(resource)]
44
+ # Add custom data in the JWT token payload
45
+ payload.merge!(resource.jwt_token_payload) if resource.respond_to?(:jwt_token_payload)
46
+
47
+ [encode(payload), new_refresh_token(resource)]
43
48
  end
44
49
 
45
50
  # Create tokens and set response headers
@@ -59,6 +64,7 @@ module ApiGuard
59
64
  # to restrict access to old access(JWT) tokens
60
65
  def invalidate_old_jwt_tokens(resource)
61
66
  return unless ApiGuard.invalidate_old_tokens_on_password_change
67
+
62
68
  resource.token_issued_at = Time.at(token_issued_at).utc
63
69
  end
64
70
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiGuard
2
4
  module JwtAuth
3
5
  # Common module for refresh token functionality
@@ -30,11 +32,13 @@ module ApiGuard
30
32
  # Create a new refresh_token for the current resource
31
33
  def new_refresh_token(resource)
32
34
  return unless refresh_token_enabled?(resource)
35
+
33
36
  refresh_tokens_for(resource).create(token: uniq_refresh_token(resource)).token
34
37
  end
35
38
 
36
39
  def destroy_all_refresh_tokens(resource)
37
40
  return unless refresh_token_enabled?(resource)
41
+
38
42
  refresh_tokens_for(resource).destroy_all
39
43
  end
40
44
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiGuard
2
4
  module Models
3
5
  module Concerns
@@ -5,19 +7,19 @@ module ApiGuard
5
7
 
6
8
  class_methods do
7
9
  def api_guard_associations(refresh_token: nil, blacklisted_token: nil)
8
- return if ApiGuard.api_guard_associations[self.name]
10
+ return if ApiGuard.api_guard_associations[name]
9
11
 
10
- ApiGuard.api_guard_associations[self.name] = {}
11
- ApiGuard.api_guard_associations[self.name][:refresh_token] = refresh_token
12
- ApiGuard.api_guard_associations[self.name][:blacklisted_token] = blacklisted_token
12
+ ApiGuard.api_guard_associations[name] = {}
13
+ ApiGuard.api_guard_associations[name][:refresh_token] = refresh_token
14
+ ApiGuard.api_guard_associations[name][:blacklisted_token] = blacklisted_token
13
15
  end
14
16
 
15
17
  def refresh_token_association
16
- ApiGuard.api_guard_associations.dig(self.name, :refresh_token)
18
+ ApiGuard.api_guard_associations.dig(name, :refresh_token)
17
19
  end
18
20
 
19
21
  def blacklisted_token_association
20
- ApiGuard.api_guard_associations.dig(self.name, :blacklisted_token)
22
+ ApiGuard.api_guard_associations.dig(name, :blacklisted_token)
21
23
  end
22
24
  end
23
25
  end
@@ -1,24 +1,26 @@
1
- require "api_guard/resource_mapper"
2
- require "api_guard/jwt_auth/json_web_token"
3
- require "api_guard/jwt_auth/authentication"
4
- require "api_guard/jwt_auth/refresh_jwt_token"
5
- require "api_guard/jwt_auth/blacklist_token"
6
- require "api_guard/response_formatters/renderer"
7
- require "api_guard/models/concerns"
1
+ # frozen_string_literal: true
2
+
3
+ require 'api_guard/resource_mapper'
4
+ require 'api_guard/jwt_auth/json_web_token'
5
+ require 'api_guard/jwt_auth/authentication'
6
+ require 'api_guard/jwt_auth/refresh_jwt_token'
7
+ require 'api_guard/jwt_auth/blacklist_token'
8
+ require 'api_guard/response_formatters/renderer'
9
+ require 'api_guard/models/concerns'
8
10
 
9
11
  module ApiGuard
10
12
  module Modules
11
- ActiveSupport.on_load(:action_controller) {
13
+ ActiveSupport.on_load(:action_controller) do
12
14
  include ApiGuard::Resource
13
15
  include ApiGuard::JwtAuth::JsonWebToken
14
16
  include ApiGuard::JwtAuth::Authentication
15
17
  include ApiGuard::JwtAuth::RefreshJwtToken
16
18
  include ApiGuard::JwtAuth::BlacklistToken
17
19
  include ApiGuard::ResponseFormatters::Renderer
18
- }
20
+ end
19
21
 
20
- ActiveSupport.on_load(:active_record) {
22
+ ActiveSupport.on_load(:active_record) do
21
23
  include ApiGuard::Models::Concerns
22
- }
24
+ end
23
25
  end
24
26
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiGuard
2
4
  class ResourceMapper
3
5
  attr_reader :resource_name, :resource_class, :resource_instance_name
@@ -19,7 +21,7 @@ module ApiGuard
19
21
  end
20
22
 
21
23
  def current_resource_mapping
22
- request.env["api_guard.mapping"]
24
+ request.env['api_guard.mapping']
23
25
  end
24
26
 
25
27
  def resource_name
@@ -1,15 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiGuard
2
4
  module ResponseFormatters
3
5
  module Renderer
4
6
  def render_success(data: nil, message: nil)
5
- resp_data = { status: 'success' }
7
+ resp_data = { status: I18n.t('api_guard.response.success') }
6
8
  resp_data[:message] = message if message
9
+ resp_data[:data] = data if data
7
10
 
8
11
  render json: resp_data, status: 200
9
12
  end
10
13
 
11
14
  def render_error(status, options = {})
12
- data = { status: 'error' }
15
+ data = { status: I18n.t('api_guard.response.error') }
13
16
  data[:error] = options[:object] ? options[:object].errors.full_messages[0] : options[:message]
14
17
 
15
18
  render json: data, status: status
@@ -1,81 +1,85 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Referenced from devise gem:
2
4
  # https://github.com/plataformatec/devise/blob/master/lib/devise/rails/routes.rb
3
5
  #
4
6
  # Customizable API routes
5
- module ActionDispatch::Routing
6
- class Mapper
7
- def api_guard_routes(options = {})
8
- routes_for = options.delete(:for).to_s || 'users'
9
-
10
- controllers = default_controllers(options[:only], options[:except])
11
- controller_options = options.delete(:controller)
12
-
13
- options[:as] = options[:as] || routes_for.singularize
14
- options[:path] = options[:path] || routes_for
15
-
16
- api_guard_scope(routes_for) do |mapped_resource|
17
- scope options do
18
- generate_routes(mapped_resource, controller_options, controllers)
7
+ module ActionDispatch
8
+ module Routing
9
+ class Mapper
10
+ def api_guard_routes(options = {})
11
+ routes_for = options.delete(:for).to_s || 'users'
12
+
13
+ controllers = default_controllers(options[:only], options[:except])
14
+ controller_options = options.delete(:controller)
15
+
16
+ options[:as] = options[:as] || routes_for.singularize
17
+ options[:path] = options[:path] || routes_for
18
+
19
+ api_guard_scope(routes_for) do |mapped_resource|
20
+ scope options do
21
+ generate_routes(mapped_resource, controller_options, controllers)
22
+ end
19
23
  end
20
24
  end
21
- end
22
25
 
23
- def api_guard_scope(routes_for)
24
- mapped_resource = ApiGuard.mapped_resource[routes_for.to_sym].presence || ApiGuard.map_resource(routes_for, routes_for.classify)
26
+ def api_guard_scope(routes_for)
27
+ mapped_resource = ApiGuard.mapped_resource[routes_for.to_sym].presence ||
28
+ ApiGuard.map_resource(routes_for, routes_for.classify)
25
29
 
26
- constraint = lambda do |request|
27
- request.env["api_guard.mapping"] = mapped_resource
28
- true
29
- end
30
+ constraint = lambda do |request|
31
+ request.env['api_guard.mapping'] = mapped_resource
32
+ true
33
+ end
30
34
 
31
- constraints(constraint) do
32
- yield(mapped_resource)
35
+ constraints(constraint) do
36
+ yield(mapped_resource)
37
+ end
33
38
  end
34
- end
35
39
 
36
- private
40
+ private
37
41
 
38
- def default_controllers(only, except)
39
- return only if only
40
-
41
- controllers = %i[registration authentication tokens passwords]
42
- except ? (controllers - except) : controllers
43
- end
42
+ def default_controllers(only, except)
43
+ return only if only
44
44
 
45
- def generate_routes(mapped_resource, options, controllers)
46
- options ||= {}
45
+ controllers = %i[registration authentication tokens passwords]
46
+ except ? (controllers - except) : controllers
47
+ end
47
48
 
48
- controllers -= %i[tokens] unless mapped_resource.resource_class.refresh_token_association
49
+ def generate_routes(mapped_resource, options, controllers)
50
+ options ||= {}
51
+ controllers -= %i[tokens] unless mapped_resource.resource_class.refresh_token_association
49
52
 
50
- controllers.each do |controller|
51
- send("#{controller.to_s}_routes", options[controller])
53
+ controllers.each do |controller|
54
+ send("#{controller}_routes", options[controller])
55
+ end
52
56
  end
53
- end
54
57
 
55
- def authentication_routes(controller_name = nil)
56
- controller_name = controller_name || 'api_guard/authentication'
58
+ def authentication_routes(controller_name = nil)
59
+ controller_name ||= 'api_guard/authentication'
57
60
 
58
- post 'sign_in' => "#{controller_name}#create"
59
- delete 'sign_out' => "#{controller_name}#destroy"
60
- end
61
+ post 'sign_in' => "#{controller_name}#create"
62
+ delete 'sign_out' => "#{controller_name}#destroy"
63
+ end
61
64
 
62
- def registration_routes(controller_name = nil)
63
- controller_name = controller_name || 'api_guard/registration'
65
+ def registration_routes(controller_name = nil)
66
+ controller_name ||= 'api_guard/registration'
64
67
 
65
- post 'sign_up' => "#{controller_name}#create"
66
- delete 'delete' => "#{controller_name}#destroy"
67
- end
68
+ post 'sign_up' => "#{controller_name}#create"
69
+ delete 'delete' => "#{controller_name}#destroy"
70
+ end
68
71
 
69
- def passwords_routes(controller_name = nil)
70
- controller_name = controller_name || 'api_guard/passwords'
72
+ def passwords_routes(controller_name = nil)
73
+ controller_name ||= 'api_guard/passwords'
71
74
 
72
- patch 'passwords' => "#{controller_name}#update"
73
- end
75
+ patch 'passwords' => "#{controller_name}#update"
76
+ end
74
77
 
75
- def tokens_routes(controller_name = nil)
76
- controller_name = controller_name || 'api_guard/tokens'
78
+ def tokens_routes(controller_name = nil)
79
+ controller_name ||= 'api_guard/tokens'
77
80
 
78
- post 'tokens' => "#{controller_name}#create"
81
+ post 'tokens' => "#{controller_name}#create"
82
+ end
79
83
  end
80
84
  end
81
85
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'api_guard/jwt_auth/json_web_token'
2
4
  require 'api_guard/jwt_auth/refresh_jwt_token'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiGuard
2
- VERSION = '0.2.1'
4
+ VERSION = '0.5.1'
3
5
  end
@@ -1,22 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ApiGuard
2
4
  class ControllersGenerator < Rails::Generators::Base
3
- CONTROLLERS = %i[registration authentication tokens passwords]
5
+ CONTROLLERS = %i[registration authentication tokens passwords].freeze
4
6
 
5
7
  desc 'Generates API Guard controllers in app/controllers/'
6
- source_root File.expand_path('../templates', __FILE__)
8
+ source_root File.expand_path('templates', __dir__)
7
9
 
8
- argument :scope, required: true,
9
- desc: "The scope to create controllers in, e.g. users, admins"
10
+ argument :scope, required: true, desc: 'The scope to create controllers in, e.g. users, admins'
10
11
 
11
- class_option :controllers, aliases: "-c", type: :array,
12
- desc: "Specify the controllers to generate (#{CONTROLLERS.join(', ')})"
12
+ class_option :controllers, aliases: '-c', type: :array,
13
+ desc: "Specify the controllers to generate (#{CONTROLLERS.join(', ')})"
13
14
 
14
15
  def create_controllers
15
16
  @controller_scope = scope.camelize
16
17
  controllers = options[:controllers] || CONTROLLERS
17
18
 
18
19
  controllers.each do |controller_name|
19
- template "#{controller_name}_controller.rb", "app/controllers/#{scope}/#{controller_name}_controller.rb"
20
+ template "#{controller_name}_controller.rb",
21
+ "app/controllers/#{scope}/#{controller_name}_controller.rb"
20
22
  end
21
23
  end
22
24
  end