api_guard_grape 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 (35) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +690 -0
  4. data/Rakefile +21 -0
  5. data/app/controllers/api_guard/application_controller.rb +11 -0
  6. data/app/controllers/api_guard/authentication_controller.rb +31 -0
  7. data/app/controllers/api_guard/passwords_controller.rb +29 -0
  8. data/app/controllers/api_guard/registration_controller.rb +30 -0
  9. data/app/controllers/api_guard/tokens_controller.rb +32 -0
  10. data/config/locales/en.yml +22 -0
  11. data/config/routes.rb +6 -0
  12. data/lib/api_guard.rb +40 -0
  13. data/lib/api_guard/app_secret_key.rb +22 -0
  14. data/lib/api_guard/engine.rb +17 -0
  15. data/lib/api_guard/jwt_auth/authentication.rb +98 -0
  16. data/lib/api_guard/jwt_auth/blacklist_token.rb +35 -0
  17. data/lib/api_guard/jwt_auth/json_web_token.rb +72 -0
  18. data/lib/api_guard/jwt_auth/refresh_jwt_token.rb +46 -0
  19. data/lib/api_guard/models/concerns.rb +27 -0
  20. data/lib/api_guard/modules.rb +26 -0
  21. data/lib/api_guard/resource_mapper.rb +43 -0
  22. data/lib/api_guard/response_formatters/renderer.rb +22 -0
  23. data/lib/api_guard/route_mapper.rb +85 -0
  24. data/lib/api_guard/test/controller_helper.rb +13 -0
  25. data/lib/api_guard/version.rb +5 -0
  26. data/lib/generators/api_guard/controllers/USAGE +11 -0
  27. data/lib/generators/api_guard/controllers/controllers_generator.rb +25 -0
  28. data/lib/generators/api_guard/controllers/templates/authentication_controller.rb +27 -0
  29. data/lib/generators/api_guard/controllers/templates/passwords_controller.rb +25 -0
  30. data/lib/generators/api_guard/controllers/templates/registration_controller.rb +26 -0
  31. data/lib/generators/api_guard/controllers/templates/tokens_controller.rb +28 -0
  32. data/lib/generators/api_guard/initializer/USAGE +8 -0
  33. data/lib/generators/api_guard/initializer/initializer_generator.rb +13 -0
  34. data/lib/generators/api_guard/initializer/templates/initializer.rb +19 -0
  35. metadata +202 -0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rdoc/task'
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = 'rdoc'
13
+ rdoc.title = 'ApiGuard'
14
+ rdoc.options << '--line-numbers'
15
+ rdoc.rdoc_files.include('README.md')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ end
18
+
19
+ load 'rails/tasks/statistics.rake'
20
+
21
+ require 'bundler/gem_tasks'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiGuard
4
+ class ApplicationController < ActionController::Base
5
+ skip_before_action :verify_authenticity_token, raise: false
6
+
7
+ def authenticate_resource
8
+ public_send("authenticate_and_set_#{resource_name}")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency 'api_guard/application_controller'
4
+
5
+ module ApiGuard
6
+ class AuthenticationController < ApplicationController
7
+ before_action :find_resource, only: [:create]
8
+ before_action :authenticate_resource, only: [:destroy]
9
+
10
+ def create
11
+ if resource.authenticate(params[:password])
12
+ create_token_and_set_header(resource, resource_name)
13
+ render_success(message: I18n.t('api_guard.authentication.signed_in'))
14
+ else
15
+ render_error(422, message: I18n.t('api_guard.authentication.invalid_login_credentials'))
16
+ end
17
+ end
18
+
19
+ def destroy
20
+ blacklist_token
21
+ render_success(message: I18n.t('api_guard.authentication.signed_out'))
22
+ end
23
+
24
+ private
25
+
26
+ def find_resource
27
+ self.resource = resource_class.find_by(email: params[:email].downcase.strip) if params[:email].present?
28
+ render_error(422, message: I18n.t('api_guard.authentication.invalid_login_credentials')) unless resource
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency 'api_guard/application_controller'
4
+
5
+ module ApiGuard
6
+ class PasswordsController < ApplicationController
7
+ before_action :authenticate_resource, only: [:update]
8
+
9
+ def update
10
+ invalidate_old_jwt_tokens(current_resource)
11
+
12
+ if current_resource.update(password_params)
13
+ blacklist_token unless ApiGuard.invalidate_old_tokens_on_password_change
14
+ destroy_all_refresh_tokens(current_resource)
15
+
16
+ create_token_and_set_header(current_resource, resource_name)
17
+ render_success(message: I18n.t('api_guard.password.changed'))
18
+ else
19
+ render_error(422, object: current_resource)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def password_params
26
+ params.permit(:password, :password_confirmation)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency 'api_guard/application_controller'
4
+
5
+ module ApiGuard
6
+ class RegistrationController < ApplicationController
7
+ before_action :authenticate_resource, only: [:destroy]
8
+
9
+ def create
10
+ init_resource(sign_up_params)
11
+ if resource.save
12
+ create_token_and_set_header(resource, resource_name)
13
+ render_success(message: I18n.t('api_guard.registration.signed_up'))
14
+ else
15
+ render_error(422, object: resource)
16
+ end
17
+ end
18
+
19
+ def destroy
20
+ current_resource.destroy
21
+ render_success(message: I18n.t('api_guard.registration.account_deleted'))
22
+ end
23
+
24
+ private
25
+
26
+ def sign_up_params
27
+ params.permit(:email, :password, :password_confirmation)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency 'api_guard/application_controller'
4
+
5
+ module ApiGuard
6
+ class TokensController < ApplicationController
7
+ before_action :authenticate_resource, only: [:create]
8
+ before_action :find_refresh_token, only: [:create]
9
+
10
+ def create
11
+ create_token_and_set_header(current_resource, resource_name)
12
+
13
+ @refresh_token.destroy
14
+ blacklist_token if ApiGuard.blacklist_token_after_refreshing
15
+
16
+ render_success(message: I18n.t('api_guard.access_token.refreshed'))
17
+ end
18
+
19
+ private
20
+
21
+ def find_refresh_token
22
+ refresh_token_from_header = request.headers['Refresh-Token']
23
+
24
+ if refresh_token_from_header
25
+ @refresh_token = find_refresh_token_of(current_resource, refresh_token_from_header)
26
+ return render_error(401, message: I18n.t('api_guard.refresh_token.invalid')) unless @refresh_token
27
+ else
28
+ render_error(401, message: I18n.t('api_guard.refresh_token.missing'))
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ en:
2
+ api_guard:
3
+ authentication:
4
+ signed_in: 'Signed in successfully'
5
+ signed_out: 'Signed out successfully'
6
+ invalid_login_credentials: 'Invalid login credentials'
7
+ password:
8
+ changed: 'Password changed successfully'
9
+ registration:
10
+ signed_up: 'Signed up successfully'
11
+ account_deleted: 'Account deleted successfully'
12
+ access_token:
13
+ refreshed: 'Token refreshed successfully'
14
+ missing: 'Access token is missing in the request'
15
+ invalid: 'Invalid access token'
16
+ expired: 'Access token expired'
17
+ refresh_token:
18
+ invalid: 'Invalid refresh token'
19
+ missing: 'Refresh token is missing in the request'
20
+ response:
21
+ success: 'success'
22
+ error: 'error'
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ ApiGuard::Engine.routes.draw do
4
+ # Can't see anything here?
5
+ # Goto 'lib/api_guard/route_mapper.rb'
6
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'api_guard/engine'
4
+ require 'api_guard/route_mapper'
5
+ require 'api_guard/modules'
6
+
7
+ module ApiGuard
8
+ autoload :AppSecretKey, 'api_guard/app_secret_key'
9
+
10
+ module Test
11
+ autoload :ControllerHelper, 'api_guard/test/controller_helper'
12
+ end
13
+
14
+ mattr_accessor :token_validity
15
+ self.token_validity = 1.day
16
+
17
+ mattr_accessor :token_signing_secret
18
+ self.token_signing_secret = nil
19
+
20
+ mattr_accessor :invalidate_old_tokens_on_password_change
21
+ self.invalidate_old_tokens_on_password_change = false
22
+
23
+ mattr_accessor :blacklist_token_after_refreshing
24
+ self.blacklist_token_after_refreshing = false
25
+
26
+ mattr_accessor :api_guard_associations
27
+ self.api_guard_associations = {}
28
+
29
+ mattr_reader :mapped_resource do
30
+ {}
31
+ end
32
+
33
+ def self.setup
34
+ yield self
35
+ end
36
+
37
+ def self.map_resource(routes_for, class_name)
38
+ mapped_resource[routes_for.to_sym] = ApiGuard::ResourceMapper.new(routes_for, class_name)
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiGuard
4
+ class AppSecretKey
5
+ def initialize(application)
6
+ @application = application
7
+ end
8
+
9
+ def detect
10
+ secret_key_base(:credentials) || secret_key_base(:secrets) ||
11
+ secret_key_base(:config) || secret_key_base
12
+ end
13
+
14
+ private
15
+
16
+ def secret_key_base(source = nil)
17
+ return @application.secret_key_base unless source
18
+
19
+ @application.send(source).secret_key_base.presence if @application.respond_to?(source)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiGuard
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace ApiGuard
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ g.fixture_replacement :factory_girl, dir: 'spec/factories'
10
+ end
11
+
12
+ # Use 'secret_key_base' from Rails secrets if 'token_signing_secret' is not configured
13
+ initializer 'ApiGuard.token_signing_secret' do |app|
14
+ ApiGuard.token_signing_secret ||= ApiGuard::AppSecretKey.new(app).detect
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiGuard
4
+ module JwtAuth
5
+ # Common module for API authentication
6
+ module Authentication
7
+ # Handle authentication of the resource dynamically
8
+ def method_missing(name, *args)
9
+ method_name = name.to_s
10
+
11
+ if method_name.start_with?('authenticate_and_set_')
12
+ resource_name = method_name.split('authenticate_and_set_')[1]
13
+ authenticate_and_set_resource(resource_name)
14
+ else
15
+ super
16
+ end
17
+ end
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
+
23
+ # Authenticate the JWT token and set resource
24
+ def authenticate_and_set_resource(resource_name)
25
+ @resource_name = resource_name
26
+
27
+ @token = request.headers['Authorization']&.split('Bearer ')&.last
28
+ return render_error(401, message: I18n.t('api_guard.access_token.missing')) unless @token
29
+
30
+ authenticate_token
31
+
32
+ # Render error response only if no resource found and no previous render happened
33
+ render_error(401, message: I18n.t('api_guard.access_token.invalid')) if !current_resource && !performed?
34
+ rescue JWT::DecodeError => e
35
+ if e.message == 'Signature has expired'
36
+ render_error(401, message: I18n.t('api_guard.access_token.expired'))
37
+ else
38
+ render_error(401, message: I18n.t('api_guard.access_token.invalid'))
39
+ end
40
+ end
41
+
42
+ # Decode the JWT token
43
+ # and don't verify token expiry for refresh token API request
44
+ def decode_token
45
+ # TODO: Set token refresh controller dynamic
46
+ verify_token = (controller_name != 'tokens' || action_name != 'create')
47
+ @decoded_token = decode(@token, verify_token)
48
+ end
49
+
50
+ # Returns whether the JWT token is issued after the last password change
51
+ # Returns true if password hasn't changed by the user
52
+ def valid_issued_at?(resource)
53
+ return true unless ApiGuard.invalidate_old_tokens_on_password_change
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
65
+ end
66
+
67
+ # Authenticate the resource with the '{{resource_name}}_id' in the decoded JWT token
68
+ # and also, check for valid issued at time and not blacklisted
69
+ #
70
+ # Also, set "current_{{resource_name}}" method and "@current_{{resource_name}}" instance variable
71
+ # for accessing the authenticated resource
72
+ def authenticate_token
73
+ return unless decode_token
74
+
75
+ resource = find_resource_from_token(@resource_name.classify.constantize)
76
+
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'))
81
+ end
82
+ end
83
+
84
+ def find_resource_from_token(resource_class)
85
+ resource_id = @decoded_token[:"#{@resource_name}_id"]
86
+ return if resource_id.blank?
87
+
88
+ resource_class.find_by(id: resource_id)
89
+ end
90
+
91
+ def current_resource
92
+ return unless respond_to?("current_#{@resource_name}")
93
+
94
+ public_send("current_#{@resource_name}")
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiGuard
4
+ module JwtAuth
5
+ # Common module for token blacklisting functionality
6
+ module BlacklistToken
7
+ def blacklisted_token_association(resource)
8
+ resource.class.blacklisted_token_association
9
+ end
10
+
11
+ def token_blacklisting_enabled?(resource)
12
+ blacklisted_token_association(resource).present?
13
+ end
14
+
15
+ def blacklisted_tokens_for(resource)
16
+ blacklisted_token_association = blacklisted_token_association(resource)
17
+ resource.send(blacklisted_token_association)
18
+ end
19
+
20
+ # Returns whether the JWT token is blacklisted or not
21
+ def blacklisted?(resource)
22
+ return false unless token_blacklisting_enabled?(resource)
23
+
24
+ blacklisted_tokens_for(resource).exists?(token: @token)
25
+ end
26
+
27
+ # Blacklist the current JWT token from future access
28
+ def blacklist_token
29
+ return unless token_blacklisting_enabled?(current_resource)
30
+
31
+ blacklisted_tokens_for(current_resource).create(token: @token, expire_at: Time.at(@decoded_token[:exp]).utc)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+
5
+ module ApiGuard
6
+ module JwtAuth
7
+ # Common module for JWT operations
8
+ module JsonWebToken
9
+ def current_time
10
+ @current_time ||= Time.now.utc
11
+ end
12
+
13
+ def token_expire_at
14
+ @token_expire_at ||= (current_time + ApiGuard.token_validity).to_i
15
+ end
16
+
17
+ def token_issued_at
18
+ @token_issued_at ||= current_time.to_i
19
+ end
20
+
21
+ # Encode the payload with the secret key and return the JWT token
22
+ def encode(payload)
23
+ JWT.encode(payload, ApiGuard.token_signing_secret)
24
+ end
25
+
26
+ # Decode the JWT token and return the payload
27
+ def decode(token, verify = true)
28
+ HashWithIndifferentAccess.new(
29
+ JWT.decode(token, ApiGuard.token_signing_secret, verify, verify_iat: true)[0]
30
+ )
31
+ end
32
+
33
+ # Create a JWT token with resource detail in payload.
34
+ # Also, create refresh token if enabled for the resource.
35
+ #
36
+ # This creates expired JWT token if the argument 'expired_token' is true which can be used for testing.
37
+ def jwt_and_refresh_token(resource, resource_name, expired_token = false)
38
+ payload = {
39
+ "#{resource_name}_id": resource.id,
40
+ exp: expired_token ? token_issued_at : token_expire_at,
41
+ iat: token_issued_at
42
+ }
43
+
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)]
48
+ end
49
+
50
+ # Create tokens and set response headers
51
+ def create_token_and_set_header(resource, resource_name)
52
+ access_token, refresh_token = jwt_and_refresh_token(resource, resource_name)
53
+ set_token_headers(access_token, refresh_token)
54
+ end
55
+
56
+ # Set token details in response headers
57
+ def set_token_headers(token, refresh_token = nil)
58
+ response.headers['Access-Token'] = token
59
+ response.headers['Refresh-Token'] = refresh_token if refresh_token
60
+ response.headers['Expire-At'] = token_expire_at.to_s
61
+ end
62
+
63
+ # Set token issued at to current timestamp
64
+ # to restrict access to old access(JWT) tokens
65
+ def invalidate_old_jwt_tokens(resource)
66
+ return unless ApiGuard.invalidate_old_tokens_on_password_change
67
+
68
+ resource.token_issued_at = Time.at(token_issued_at).utc
69
+ end
70
+ end
71
+ end
72
+ end