api_guard_grape 0.5.1

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