api_guard 0.2.1 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: db273f8953347d07ed9d5acb5923480e463461517e2e1396d37a9db704ffc218
|
4
|
+
data.tar.gz: 92b5aeffc41c1d97d3d1ade1389b4acd902ff66166ecb3895fd858b0f8f51a01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d99aa923a6b41bdc4b1c5524cc9159342fee8e1c31931d0387c21651d7b338ccef6be7773cdd48d8773f1b50000e228310bf7a18d9c3cefaeb52f63f2b82c824
|
7
|
+
data.tar.gz: 3651814062e401e8c8be16d9c103a23de042232140bccb10a79977dc3ffdb7370b658ddfeed26cc3e995705e3d7e60c90d97f5b65469025eed717260e6097a31
|
data/README.md
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
# API Guard
|
2
2
|
|
3
3
|
[![Version](https://img.shields.io/gem/v/api_guard.svg?color=green)](https://rubygems.org/gems/api_guard)
|
4
|
-
[![Build Status](https://
|
4
|
+
[![Build Status](https://github.com/Gokul595/api_guard/workflows/build/badge.svg?branch=master)](https://github.com/Gokul595/api_guard/actions?query=workflow%3Abuild)
|
5
5
|
[![Maintainability](https://api.codeclimate.com/v1/badges/ced3e74a26a66ed915cb/maintainability)](https://codeclimate.com/github/Gokul595/api_guard/maintainability)
|
6
|
-
[![Test Coverage](https://api.codeclimate.com/v1/badges/ced3e74a26a66ed915cb/test_coverage)](https://codeclimate.com/github/Gokul595/api_guard/test_coverage)
|
7
6
|
|
8
7
|
|
9
8
|
[JSON Web Token (JWT)](https://jwt.io/) based authentication solution with token refreshing & blacklisting for APIs
|
@@ -35,7 +34,12 @@ for cryptographic signing.
|
|
35
34
|
* [Overriding defaults](#overriding-defaults)
|
36
35
|
* [Controllers](#controllers)
|
37
36
|
* [Routes](#routes)
|
37
|
+
* [Adding custom data in JWT token payload](#adding-custom-data-in-jwt-token-payload)
|
38
|
+
* [Override finding resource](#override-finding-resource)
|
39
|
+
* [Customizing / translating response messages using I18n](#customizing--translating-response-messages-using-i18n)
|
38
40
|
* [Testing](#testing)
|
41
|
+
* [Wiki](https://github.com/Gokul595/api_guard/wiki)
|
42
|
+
* [Using API Guard with Devise](https://github.com/Gokul595/api_guard/wiki/Using-API-Guard-with-Devise)
|
39
43
|
* [Contributing](#contributing)
|
40
44
|
* [License](#license)
|
41
45
|
|
@@ -63,20 +67,22 @@ Below steps are provided assuming the model in `User`.
|
|
63
67
|
|
64
68
|
### Creating User model
|
65
69
|
|
66
|
-
Create a model for User with below command
|
70
|
+
Create a model for User with below command.
|
67
71
|
|
68
72
|
```bash
|
69
73
|
$ rails generate model user name:string email:string:uniq password_digest:string
|
70
74
|
```
|
71
75
|
|
72
|
-
Then, run migration to create the `users` table
|
76
|
+
Then, run migration to create the `users` table.
|
73
77
|
|
74
78
|
```bash
|
75
79
|
$ rails db:migrate
|
76
80
|
```
|
77
81
|
|
78
82
|
Add [has_secure_password](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password)
|
79
|
-
in `User` model for password authentication
|
83
|
+
in `User` model for password authentication.
|
84
|
+
|
85
|
+
> Refer [this Wiki](https://github.com/Gokul595/api_guard/wiki/Using-API-Guard-with-Devise#authentication) for configuring API Guard authentication to work with Devise instead of using `has_secure_password`.
|
80
86
|
|
81
87
|
```ruby
|
82
88
|
class User < ApplicationRecord
|
@@ -86,7 +92,7 @@ end
|
|
86
92
|
|
87
93
|
Then, add `bcrypt` gem in your Gemfile which is used by
|
88
94
|
[has_secure_password](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password)
|
89
|
-
for encrypting password and authentication
|
95
|
+
for encrypting password and authentication.
|
90
96
|
|
91
97
|
```ruby
|
92
98
|
gem 'bcrypt', '~> 3.1.7'
|
@@ -108,6 +114,8 @@ api_guard_routes for: 'users'
|
|
108
114
|
|
109
115
|
This will generate default routes such as sign up, sign in, sign out, token refresh, password change for User.
|
110
116
|
|
117
|
+
> Refer [this Wiki](https://github.com/Gokul595/api_guard/wiki/Using-API-Guard-with-Devise#routes) for configuring API Guard routes to work with Devise.
|
118
|
+
|
111
119
|
### Registration
|
112
120
|
|
113
121
|
This will create an user and responds with access token, refresh token and access token expiry in the response header.
|
@@ -353,19 +361,19 @@ configurations.
|
|
353
361
|
ApiGuard.setup do |config|
|
354
362
|
# Validity of the JWT access token
|
355
363
|
# Default: 1 day
|
356
|
-
config.token_validity = 1.day
|
364
|
+
# config.token_validity = 1.day
|
357
365
|
|
358
366
|
# Secret key for signing (encoding & decoding) the JWT access token
|
359
367
|
# Default: 'secret_key_base' from Rails secrets
|
360
|
-
config.token_signing_secret =
|
368
|
+
# config.token_signing_secret = 'my_signing_secret'
|
361
369
|
|
362
370
|
# Invalidate old tokens on changing the password
|
363
371
|
# Default: false
|
364
|
-
config.invalidate_old_tokens_on_password_change = false
|
372
|
+
# config.invalidate_old_tokens_on_password_change = false
|
365
373
|
|
366
374
|
# Blacklist JWT access token after refreshing
|
367
375
|
# Default: false
|
368
|
-
config.blacklist_token_after_refreshing = false
|
376
|
+
# config.blacklist_token_after_refreshing = false
|
369
377
|
end
|
370
378
|
```
|
371
379
|
|
@@ -389,6 +397,10 @@ Override this by configuring `token_signing_secret`
|
|
389
397
|
config.token_signing_secret = 'my_signing_secret'
|
390
398
|
```
|
391
399
|
|
400
|
+
>**Note:** Avoid committing this token signing secret in your version control (GIT) and always keep this secure. As,
|
401
|
+
>exposing this allow anyone to generate JWT access token and give full access to APIs. Better way is storing this value
|
402
|
+
>in environment variable or in encrypted secrets (Rails 5.2+)
|
403
|
+
|
392
404
|
### Invalidate tokens on password change
|
393
405
|
|
394
406
|
By default, API Guard will not invalidate old JWT access tokens on changing password. If you need, you can enable it by
|
@@ -539,28 +551,96 @@ api_guard_routes for: 'users', only: [:authentication]
|
|
539
551
|
|
540
552
|
>**Available controllers:** registration, authentication, tokens, passwords
|
541
553
|
|
542
|
-
**Customizing the
|
554
|
+
**Customizing the route path:**
|
543
555
|
|
544
|
-
|
556
|
+
You can customize the path of the default routes of the API Guard using the `api_guard_scope` as below,
|
545
557
|
|
546
558
|
```ruby
|
547
559
|
api_guard_routes for: 'users', except: [:registration]
|
548
560
|
|
549
561
|
api_guard_scope 'users' do
|
550
|
-
post 'account/create' => '
|
551
|
-
delete 'account/delete' => '
|
562
|
+
post 'account/create' => 'api_guard/registration#create'
|
563
|
+
delete 'account/delete' => 'api_guard/registration#destroy'
|
552
564
|
end
|
553
565
|
```
|
554
566
|
|
555
567
|
Above configuration will replace default registration routes `users/sign_up` & `users/delete` with `account/create` &
|
556
568
|
`account/delete`
|
557
569
|
|
570
|
+
### Adding custom data in JWT token payload
|
571
|
+
|
572
|
+
You can add custom data in the JWT token payload in the format of Hash and use the data after decoding the token on
|
573
|
+
every request.
|
574
|
+
|
575
|
+
To add custom data, you need to create an instance method `jwt_token_payload` in the resource model as below which
|
576
|
+
should return a Hash,
|
577
|
+
|
578
|
+
```ruby
|
579
|
+
class User < ApplicationRecord
|
580
|
+
def jwt_token_payload
|
581
|
+
{ custom_key: 'value' }
|
582
|
+
end
|
583
|
+
end
|
584
|
+
```
|
585
|
+
|
586
|
+
API Guard will add the hash returned by this method to the JWT token payload in addition to the default payload values.
|
587
|
+
This data (including default payload values) will be available in the instance variable `@decoded_token` on each request
|
588
|
+
if the token has been successfully decoded. You can access the values as below,
|
589
|
+
|
590
|
+
```ruby
|
591
|
+
@decoded_token[:custom_key]
|
592
|
+
```
|
593
|
+
|
594
|
+
### Override finding resource
|
595
|
+
|
596
|
+
By default, API Guard will try to find the resource by it's `id`. If you wish to override this default behavior, you can
|
597
|
+
do it by creating a method `find_resource_from_token` in the specific controller or in `ApplicationController` as you
|
598
|
+
need.
|
599
|
+
|
600
|
+
**Adding custom logic in addition to the default logic:**
|
601
|
+
```ruby
|
602
|
+
def find_resource_from_token(resource_class)
|
603
|
+
user = super # This will call the actual method defined in API Guard
|
604
|
+
user if user&.active?
|
605
|
+
end
|
606
|
+
```
|
607
|
+
|
608
|
+
**Using custom query to find the user from the token:**
|
609
|
+
```ruby
|
610
|
+
def find_resource_from_token(resource_class)
|
611
|
+
resource_id = @decoded_token[:"#{@resource_name}_id"]
|
612
|
+
resource_class.find_by(id: resource_id, status: 'active') if resource_id
|
613
|
+
end
|
614
|
+
```
|
615
|
+
|
616
|
+
This method has an argument `resource_class` which is the class (model) of the current resource (`User`).
|
617
|
+
This method should return a resource object to successfully authenticate the request or `nil` to respond with 401.
|
618
|
+
|
619
|
+
You can also use the [custom data](#adding-custom-data-in-jwt-token-payload) added in the JWT token payload using
|
620
|
+
`@decoded_token` instance variable and customize the logic as you need.
|
621
|
+
|
622
|
+
### Customizing / translating response messages using I18n
|
623
|
+
|
624
|
+
API Guard uses [I18n](https://guides.rubyonrails.org/i18n.html) for success and error messages. You can create your own
|
625
|
+
locale file and customize the messages for any language.
|
626
|
+
|
627
|
+
```yaml
|
628
|
+
en:
|
629
|
+
api_guard:
|
630
|
+
authentication:
|
631
|
+
signed_in: 'Signed in successfully'
|
632
|
+
signed_out: 'Signed out successfully'
|
633
|
+
```
|
634
|
+
|
635
|
+
You can find the complete list of available keys in this file:
|
636
|
+
https://github.com/Gokul595/api_guard/blob/master/config/locales/en.yml
|
637
|
+
|
558
638
|
## Testing
|
559
639
|
|
560
640
|
API Guard comes with helper for creating JWT access token and refresh token for the resource which you can use it for
|
561
641
|
testing the controllers of your application.
|
562
642
|
|
563
|
-
For using it include the helper in
|
643
|
+
For using it, just include the helper in your test framework.
|
564
644
|
|
565
645
|
**RSpec**
|
566
646
|
|
data/Rakefile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
begin
|
2
4
|
require 'bundler/setup'
|
3
5
|
rescue LoadError
|
@@ -14,11 +16,6 @@ RDoc::Task.new(:rdoc) do |rdoc|
|
|
14
16
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
17
|
end
|
16
18
|
|
17
|
-
|
18
|
-
|
19
19
|
load 'rails/tasks/statistics.rake'
|
20
20
|
|
21
|
-
|
22
|
-
|
23
21
|
require 'bundler/gem_tasks'
|
24
|
-
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_dependency 'api_guard/application_controller'
|
2
4
|
|
3
5
|
module ApiGuard
|
@@ -8,22 +10,22 @@ module ApiGuard
|
|
8
10
|
def create
|
9
11
|
if resource.authenticate(params[:password])
|
10
12
|
create_token_and_set_header(resource, resource_name)
|
11
|
-
render_success(message: '
|
13
|
+
render_success(message: I18n.t('api_guard.authentication.signed_in'))
|
12
14
|
else
|
13
|
-
render_error(422, message: '
|
15
|
+
render_error(422, message: I18n.t('api_guard.authentication.invalid_login_credentials'))
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
17
19
|
def destroy
|
18
20
|
blacklist_token
|
19
|
-
render_success(message: '
|
21
|
+
render_success(message: I18n.t('api_guard.authentication.signed_out'))
|
20
22
|
end
|
21
23
|
|
22
24
|
private
|
23
25
|
|
24
26
|
def find_resource
|
25
27
|
self.resource = resource_class.find_by(email: params[:email].downcase.strip) if params[:email].present?
|
26
|
-
render_error(422, message: '
|
28
|
+
render_error(422, message: I18n.t('api_guard.authentication.invalid_login_credentials')) unless resource
|
27
29
|
end
|
28
30
|
end
|
29
31
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_dependency 'api_guard/application_controller'
|
2
4
|
|
3
5
|
module ApiGuard
|
@@ -7,12 +9,12 @@ module ApiGuard
|
|
7
9
|
def update
|
8
10
|
invalidate_old_jwt_tokens(current_resource)
|
9
11
|
|
10
|
-
if current_resource.
|
12
|
+
if current_resource.update(password_params)
|
11
13
|
blacklist_token unless ApiGuard.invalidate_old_tokens_on_password_change
|
12
14
|
destroy_all_refresh_tokens(current_resource)
|
13
15
|
|
14
16
|
create_token_and_set_header(current_resource, resource_name)
|
15
|
-
render_success(message: '
|
17
|
+
render_success(message: I18n.t('api_guard.password.changed'))
|
16
18
|
else
|
17
19
|
render_error(422, object: current_resource)
|
18
20
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_dependency 'api_guard/application_controller'
|
2
4
|
|
3
5
|
module ApiGuard
|
@@ -8,7 +10,7 @@ module ApiGuard
|
|
8
10
|
init_resource(sign_up_params)
|
9
11
|
if resource.save
|
10
12
|
create_token_and_set_header(resource, resource_name)
|
11
|
-
render_success(message: '
|
13
|
+
render_success(message: I18n.t('api_guard.registration.signed_up'))
|
12
14
|
else
|
13
15
|
render_error(422, object: resource)
|
14
16
|
end
|
@@ -16,7 +18,7 @@ module ApiGuard
|
|
16
18
|
|
17
19
|
def destroy
|
18
20
|
current_resource.destroy
|
19
|
-
render_success(message:
|
21
|
+
render_success(message: I18n.t('api_guard.registration.account_deleted'))
|
20
22
|
end
|
21
23
|
|
22
24
|
private
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_dependency 'api_guard/application_controller'
|
2
4
|
|
3
5
|
module ApiGuard
|
@@ -11,7 +13,7 @@ module ApiGuard
|
|
11
13
|
@refresh_token.destroy
|
12
14
|
blacklist_token if ApiGuard.blacklist_token_after_refreshing
|
13
15
|
|
14
|
-
render_success(message: '
|
16
|
+
render_success(message: I18n.t('api_guard.access_token.refreshed'))
|
15
17
|
end
|
16
18
|
|
17
19
|
private
|
@@ -21,9 +23,9 @@ module ApiGuard
|
|
21
23
|
|
22
24
|
if refresh_token_from_header
|
23
25
|
@refresh_token = find_refresh_token_of(current_resource, refresh_token_from_header)
|
24
|
-
return render_error(401, message: '
|
26
|
+
return render_error(401, message: I18n.t('api_guard.refresh_token.invalid')) unless @refresh_token
|
25
27
|
else
|
26
|
-
render_error(401, message: '
|
28
|
+
render_error(401, message: I18n.t('api_guard.refresh_token.missing'))
|
27
29
|
end
|
28
30
|
end
|
29
31
|
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'
|
data/config/routes.rb
CHANGED
data/lib/api_guard.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'api_guard/engine'
|
4
|
+
require 'api_guard/route_mapper'
|
5
|
+
require 'api_guard/modules'
|
4
6
|
|
5
7
|
module ApiGuard
|
8
|
+
autoload :AppSecretKey, 'api_guard/app_secret_key'
|
9
|
+
|
6
10
|
module Test
|
7
11
|
autoload :ControllerHelper, 'api_guard/test/controller_helper'
|
8
12
|
end
|
@@ -22,14 +26,15 @@ module ApiGuard
|
|
22
26
|
mattr_accessor :api_guard_associations
|
23
27
|
self.api_guard_associations = {}
|
24
28
|
|
25
|
-
mattr_reader :mapped_resource
|
26
|
-
|
29
|
+
mattr_reader :mapped_resource do
|
30
|
+
{}
|
31
|
+
end
|
27
32
|
|
28
33
|
def self.setup
|
29
34
|
yield self
|
30
35
|
end
|
31
36
|
|
32
37
|
def self.map_resource(routes_for, class_name)
|
33
|
-
|
38
|
+
mapped_resource[routes_for.to_sym] = ApiGuard::ResourceMapper.new(routes_for, class_name)
|
34
39
|
end
|
35
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
|
data/lib/api_guard/engine.rb
CHANGED
@@ -1,18 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ApiGuard
|
2
4
|
class Engine < ::Rails::Engine
|
3
5
|
isolate_namespace ApiGuard
|
4
6
|
|
5
7
|
config.generators do |g|
|
6
8
|
g.test_framework :rspec
|
7
|
-
g.fixture_replacement :factory_girl, :
|
9
|
+
g.fixture_replacement :factory_girl, dir: 'spec/factories'
|
8
10
|
end
|
9
11
|
|
10
12
|
# Use 'secret_key_base' from Rails secrets if 'token_signing_secret' is not configured
|
11
13
|
initializer 'ApiGuard.token_signing_secret' do |app|
|
12
|
-
|
13
|
-
signing_secret = app.respond_to?(:credentials) ? app.credentials.secret_key_base : app.secrets.secret_key_base
|
14
|
-
ApiGuard.token_signing_secret = signing_secret
|
15
|
-
end
|
14
|
+
ApiGuard.token_signing_secret ||= ApiGuard::AppSecretKey.new(app).detect
|
16
15
|
end
|
17
16
|
end
|
18
17
|
end
|