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.
- checksums.yaml +5 -5
- data/README.md +95 -15
- data/Rakefile +2 -5
- data/app/controllers/api_guard/application_controller.rb +2 -0
- data/app/controllers/api_guard/authentication_controller.rb +6 -4
- data/app/controllers/api_guard/passwords_controller.rb +4 -2
- data/app/controllers/api_guard/registration_controller.rb +4 -2
- data/app/controllers/api_guard/tokens_controller.rb +5 -3
- data/config/locales/en.yml +22 -0
- data/config/routes.rb +2 -0
- data/lib/api_guard.rb +11 -6
- data/lib/api_guard/app_secret_key.rb +22 -0
- data/lib/api_guard/engine.rb +4 -5
- data/lib/api_guard/jwt_auth/authentication.rb +34 -12
- data/lib/api_guard/jwt_auth/blacklist_token.rb +7 -3
- data/lib/api_guard/jwt_auth/json_web_token.rb +11 -5
- data/lib/api_guard/jwt_auth/refresh_jwt_token.rb +4 -0
- data/lib/api_guard/models/concerns.rb +8 -6
- data/lib/api_guard/modules.rb +13 -11
- data/lib/api_guard/resource_mapper.rb +3 -1
- data/lib/api_guard/response_formatters/renderer.rb +5 -2
- data/lib/api_guard/route_mapper.rb +58 -54
- data/lib/api_guard/test/controller_helper.rb +2 -0
- data/lib/api_guard/version.rb +3 -1
- data/lib/generators/api_guard/controllers/controllers_generator.rb +9 -7
- data/lib/generators/api_guard/controllers/templates/authentication_controller.rb +4 -4
- data/lib/generators/api_guard/controllers/templates/passwords_controller.rb +3 -3
- data/lib/generators/api_guard/controllers/templates/registration_controller.rb +3 -3
- data/lib/generators/api_guard/controllers/templates/tokens_controller.rb +7 -4
- data/lib/generators/api_guard/initializer/initializer_generator.rb +3 -1
- data/lib/generators/api_guard/initializer/templates/initializer.rb +6 -4
- metadata +54 -69
- data/app/models/api_guard/application_record.rb +0 -5
- 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: '
|
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: '
|
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: '
|
36
|
+
render_error(401, message: I18n.t('api_guard.access_token.expired'))
|
31
37
|
else
|
32
|
-
render_error(401, message: '
|
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
|
-
|
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
|
73
|
+
return unless decode_token
|
58
74
|
|
59
|
-
resource = @resource_name.classify.constantize
|
75
|
+
resource = find_resource_from_token(@resource_name.classify.constantize)
|
60
76
|
|
61
|
-
|
62
|
-
|
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
|
-
|
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
|
-
|
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?(
|
21
|
-
|
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
|
-
@
|
14
|
+
@token_expire_at ||= (current_time + ApiGuard.token_validity).to_i
|
13
15
|
end
|
14
16
|
|
15
17
|
def token_issued_at
|
16
|
-
@
|
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
|
-
|
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
|
-
|
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[
|
10
|
+
return if ApiGuard.api_guard_associations[name]
|
9
11
|
|
10
|
-
ApiGuard.api_guard_associations[
|
11
|
-
ApiGuard.api_guard_associations[
|
12
|
-
ApiGuard.api_guard_associations[
|
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(
|
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(
|
22
|
+
ApiGuard.api_guard_associations.dig(name, :blacklisted_token)
|
21
23
|
end
|
22
24
|
end
|
23
25
|
end
|
data/lib/api_guard/modules.rb
CHANGED
@@ -1,24 +1,26 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
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[
|
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
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
24
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
+
constraint = lambda do |request|
|
31
|
+
request.env['api_guard.mapping'] = mapped_resource
|
32
|
+
true
|
33
|
+
end
|
30
34
|
|
31
|
-
|
32
|
-
|
35
|
+
constraints(constraint) do
|
36
|
+
yield(mapped_resource)
|
37
|
+
end
|
33
38
|
end
|
34
|
-
end
|
35
39
|
|
36
|
-
|
40
|
+
private
|
37
41
|
|
38
|
-
|
39
|
-
|
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
|
-
|
46
|
-
|
45
|
+
controllers = %i[registration authentication tokens passwords]
|
46
|
+
except ? (controllers - except) : controllers
|
47
|
+
end
|
47
48
|
|
48
|
-
|
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
|
-
|
51
|
-
|
53
|
+
controllers.each do |controller|
|
54
|
+
send("#{controller}_routes", options[controller])
|
55
|
+
end
|
52
56
|
end
|
53
|
-
end
|
54
57
|
|
55
|
-
|
56
|
-
|
58
|
+
def authentication_routes(controller_name = nil)
|
59
|
+
controller_name ||= 'api_guard/authentication'
|
57
60
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
+
post 'sign_in' => "#{controller_name}#create"
|
62
|
+
delete 'sign_out' => "#{controller_name}#destroy"
|
63
|
+
end
|
61
64
|
|
62
|
-
|
63
|
-
|
65
|
+
def registration_routes(controller_name = nil)
|
66
|
+
controller_name ||= 'api_guard/registration'
|
64
67
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
+
post 'sign_up' => "#{controller_name}#create"
|
69
|
+
delete 'delete' => "#{controller_name}#destroy"
|
70
|
+
end
|
68
71
|
|
69
|
-
|
70
|
-
|
72
|
+
def passwords_routes(controller_name = nil)
|
73
|
+
controller_name ||= 'api_guard/passwords'
|
71
74
|
|
72
|
-
|
73
|
-
|
75
|
+
patch 'passwords' => "#{controller_name}#update"
|
76
|
+
end
|
74
77
|
|
75
|
-
|
76
|
-
|
78
|
+
def tokens_routes(controller_name = nil)
|
79
|
+
controller_name ||= 'api_guard/tokens'
|
77
80
|
|
78
|
-
|
81
|
+
post 'tokens' => "#{controller_name}#create"
|
82
|
+
end
|
79
83
|
end
|
80
84
|
end
|
81
85
|
end
|
data/lib/api_guard/version.rb
CHANGED
@@ -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('
|
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:
|
12
|
-
|
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",
|
20
|
+
template "#{controller_name}_controller.rb",
|
21
|
+
"app/controllers/#{scope}/#{controller_name}_controller.rb"
|
20
22
|
end
|
21
23
|
end
|
22
24
|
end
|