api_guard_grape 0.5.1 → 0.5.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8fa6b87a02bcff0944fd185a949d84c63776d678928ca896fd66ea8b1ad4992a
4
- data.tar.gz: 29f4c5f87ca08a83ed96ec88f1b186ff9e76e0759b6748e1a603534e2ab16bd4
3
+ metadata.gz: 5cbdbe6a99ddb95a194ca2d457a00c632dcd8e70731b6e8bcebf02388c32a7ed
4
+ data.tar.gz: ded2f1d9537467657b493269e3a06093b2208f871a356f056aaf1179cd8e6e85
5
5
  SHA512:
6
- metadata.gz: 9981e401bbd55f0514320a1bf94aeba015f98c38c2b5a2d14cc10a745a487a52ccb134a4ce9043817fdc84c4974340a6bea9e682740c1ece82172ac9654cfae5
7
- data.tar.gz: 03efd2036116ee5e4fd46b08e7ee08635a702b08e2d5ff9d2d7c36745ede88a89df8197b0db5a39a36b99a63a831d08c283b59e45cca68acc114587024fb8bf8
6
+ metadata.gz: 37e5bb2e16a4d3733b630577c070be248fc9d24aa70209434fd0709b2c6efa60b32e3a90b76d08d4d7f127e3a2e0ef48b7ff4832bfd66a1f89f1120a911020ab
7
+ data.tar.gz: 97c8f918a4de68e43c42b207aeb964a8cd5b7c2ff12658533a61db86921b7b2cdd0e6fcd8f4467faa9e21a9706c527a05afe1f3fe440b4b962c00b99892f3d4e
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # API Guard
2
2
 
3
- [![Version](https://img.shields.io/gem/v/api_guard.svg?color=green)](https://rubygems.org/gems/api_guard_grape)
4
- [![Build Status](https://github.com/prateeksinghbundela/api_guard_grape/workflows/build/badge.svg?branch=master)](https://github.com/prateeksinghbundela/api_guard_grape/actions?query=workflow%3Abuild)
5
- [![Maintainability](https://api.codeclimate.com/v1/badges/ced3e74a26a66ed915cb/maintainability)](https://codeclimate.com/github/prateeksinghbundela/api_guard_grape/maintainability)
3
+ [![Version](https://img.shields.io/gem/v/api_guard.svg?color=green)](https://rubygems.org/gems/api_guard)
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
+ [![Maintainability](https://api.codeclimate.com/v1/badges/ced3e74a26a66ed915cb/maintainability)](https://codeclimate.com/github/Gokul595/api_guard/maintainability)
6
6
 
7
7
 
8
8
  [JSON Web Token (JWT)](https://jwt.io/) based authentication solution with token refreshing & blacklisting for APIs
@@ -38,8 +38,8 @@ for cryptographic signing.
38
38
  * [Override finding resource](#override-finding-resource)
39
39
  * [Customizing / translating response messages using I18n](#customizing--translating-response-messages-using-i18n)
40
40
  * [Testing](#testing)
41
- * [Wiki](https://github.com/prateeksinghbundela/api_guard_grape/wiki)
42
- * [Using API Guard with Devise](https://github.com/prateeksinghbundela/api_guard_grape/wiki/Using-API-Guard-with-Devise)
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)
43
43
  * [Contributing](#contributing)
44
44
  * [License](#license)
45
45
 
@@ -48,7 +48,7 @@ for cryptographic signing.
48
48
  Add this line to your application's Gemfile:
49
49
 
50
50
  ```ruby
51
- gem 'api_guard_grape'
51
+ gem 'api_guard'
52
52
  ```
53
53
 
54
54
  And then execute in your terminal:
@@ -58,7 +58,7 @@ $ bundle install
58
58
 
59
59
  Or install it yourself as:
60
60
  ```bash
61
- $ gem install api_guard_grape
61
+ $ gem install api_guard
62
62
  ```
63
63
 
64
64
  ## Getting Started
@@ -82,7 +82,7 @@ $ rails db:migrate
82
82
  Add [has_secure_password](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password)
83
83
  in `User` model for password authentication.
84
84
 
85
- > Refer [this Wiki](https://github.com/prateeksinghbundela/api_guard_grape/wiki/Using-API-Guard-with-Devise#authentication) for configuring API Guard authentication to work with Devise instead of using `has_secure_password`.
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`.
86
86
 
87
87
  ```ruby
88
88
  class User < ApplicationRecord
@@ -114,7 +114,7 @@ api_guard_routes for: 'users'
114
114
 
115
115
  This will generate default routes such as sign up, sign in, sign out, token refresh, password change for User.
116
116
 
117
- > Refer [this Wiki](https://github.com/prateeksinghbundela/api_guard_grape/wiki/Using-API-Guard-with-Devise#routes) for configuring API Guard routes to work with Devise.
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
118
 
119
119
  ### Registration
120
120
 
@@ -633,7 +633,7 @@ en:
633
633
  ```
634
634
 
635
635
  You can find the complete list of available keys in this file:
636
- https://github.com/prateeksinghbundela/api_guard_grape/blob/master/config/locales/en.yml
636
+ https://github.com/Gokul595/api_guard/blob/master/config/locales/en.yml
637
637
 
638
638
  ## Testing
639
639
 
@@ -680,7 +680,7 @@ Then, you can set the access token and refresh token in appropriate request head
680
680
 
681
681
  ## Contributing
682
682
 
683
- Bug reports and pull requests are welcome on GitHub at https://github.com/prateeksinghbundela/api_guard_grape.
683
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Gokul595/api_guard.
684
684
  This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to
685
685
  the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
686
686
 
@@ -4,28 +4,28 @@ module ApiGuard
4
4
  module JwtAuth
5
5
  # Common module for token blacklisting functionality
6
6
  module BlacklistToken
7
- def blacklisted_token_association(resource)
7
+ def self.blacklisted_token_association(resource)
8
8
  resource.class.blacklisted_token_association
9
9
  end
10
10
 
11
- def token_blacklisting_enabled?(resource)
11
+ def self.token_blacklisting_enabled?(resource)
12
12
  blacklisted_token_association(resource).present?
13
13
  end
14
14
 
15
- def blacklisted_tokens_for(resource)
15
+ def self.blacklisted_tokens_for(resource)
16
16
  blacklisted_token_association = blacklisted_token_association(resource)
17
17
  resource.send(blacklisted_token_association)
18
18
  end
19
19
 
20
20
  # Returns whether the JWT token is blacklisted or not
21
- def blacklisted?(resource)
21
+ def self.blacklisted?(resource)
22
22
  return false unless token_blacklisting_enabled?(resource)
23
23
 
24
24
  blacklisted_tokens_for(resource).exists?(token: @token)
25
25
  end
26
26
 
27
27
  # Blacklist the current JWT token from future access
28
- def blacklist_token
28
+ def self.blacklist_token
29
29
  return unless token_blacklisting_enabled?(current_resource)
30
30
 
31
31
  blacklisted_tokens_for(current_resource).create(token: @token, expire_at: Time.at(@decoded_token[:exp]).utc)
@@ -1,30 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'jwt'
4
-
4
+ require 'api_guard/jwt_auth/refresh_jwt_token'
5
5
  module ApiGuard
6
6
  module JwtAuth
7
7
  # Common module for JWT operations
8
8
  module JsonWebToken
9
- def current_time
9
+
10
+ def self.current_time
10
11
  @current_time ||= Time.now.utc
11
12
  end
12
13
 
13
- def token_expire_at
14
+ def self.token_expire_at
14
15
  @token_expire_at ||= (current_time + ApiGuard.token_validity).to_i
15
16
  end
16
17
 
17
- def token_issued_at
18
+ def self.token_issued_at
18
19
  @token_issued_at ||= current_time.to_i
19
20
  end
20
21
 
21
22
  # Encode the payload with the secret key and return the JWT token
22
- def encode(payload)
23
+ def self.encode(payload)
23
24
  JWT.encode(payload, ApiGuard.token_signing_secret)
24
25
  end
25
26
 
26
27
  # Decode the JWT token and return the payload
27
- def decode(token, verify = true)
28
+ def self.decode(token, verify = true)
28
29
  HashWithIndifferentAccess.new(
29
30
  JWT.decode(token, ApiGuard.token_signing_secret, verify, verify_iat: true)[0]
30
31
  )
@@ -34,7 +35,7 @@ module ApiGuard
34
35
  # Also, create refresh token if enabled for the resource.
35
36
  #
36
37
  # 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
+ def self.jwt_and_refresh_token(resource, resource_name, expired_token = false)
38
39
  payload = {
39
40
  "#{resource_name}_id": resource.id,
40
41
  exp: expired_token ? token_issued_at : token_expire_at,
@@ -43,18 +44,17 @@ module ApiGuard
43
44
 
44
45
  # Add custom data in the JWT token payload
45
46
  payload.merge!(resource.jwt_token_payload) if resource.respond_to?(:jwt_token_payload)
46
-
47
- [encode(payload), new_refresh_token(resource)]
47
+ [self.encode(payload), self.new_refresh_token(resource)]
48
48
  end
49
49
 
50
50
  # Create tokens and set response headers
51
- def create_token_and_set_header(resource, resource_name)
51
+ def self.create_token_and_set_header(resource, resource_name)
52
52
  access_token, refresh_token = jwt_and_refresh_token(resource, resource_name)
53
53
  set_token_headers(access_token, refresh_token)
54
54
  end
55
55
 
56
56
  # Set token details in response headers
57
- def set_token_headers(token, refresh_token = nil)
57
+ def self.set_token_headers(token, refresh_token = nil)
58
58
  response.headers['Access-Token'] = token
59
59
  response.headers['Refresh-Token'] = refresh_token if refresh_token
60
60
  response.headers['Expire-At'] = token_expire_at.to_s
@@ -62,11 +62,169 @@ module ApiGuard
62
62
 
63
63
  # Set token issued at to current timestamp
64
64
  # to restrict access to old access(JWT) tokens
65
- def invalidate_old_jwt_tokens(resource)
65
+ def self.invalidate_old_jwt_tokens(resource)
66
66
  return unless ApiGuard.invalidate_old_tokens_on_password_change
67
67
 
68
68
  resource.token_issued_at = Time.at(token_issued_at).utc
69
69
  end
70
+
71
+ #refresh token code=======================================
72
+
73
+ def self.refresh_token_association(resource)
74
+ resource.class.refresh_token_association
75
+ end
76
+
77
+ def self.refresh_token_enabled?(resource)
78
+ refresh_token_association(resource).present?
79
+ end
80
+
81
+ def self.refresh_tokens_for(resource)
82
+ refresh_token_association = refresh_token_association(resource)
83
+ resource.send(refresh_token_association)
84
+ end
85
+
86
+ def self.find_refresh_token_of(resource, refresh_token)
87
+ refresh_tokens_for(resource).find_by_token(refresh_token)
88
+ end
89
+
90
+ # Generate and return unique refresh token for the resource
91
+ def self.uniq_refresh_token(resource)
92
+ loop do
93
+ random_token = SecureRandom.urlsafe_base64
94
+ return random_token unless refresh_tokens_for(resource).exists?(token: random_token)
95
+ end
96
+ end
97
+
98
+ # Create a new refresh_token for the current resource
99
+ def self.new_refresh_token(resource)
100
+ return unless refresh_token_enabled?(resource)
101
+
102
+ refresh_tokens_for(resource).create(token: uniq_refresh_token(resource)).token
103
+ end
104
+
105
+ def self.destroy_all_refresh_tokens(resource)
106
+ return unless refresh_token_enabled?(resource)
107
+
108
+ refresh_tokens_for(resource).destroy_all
109
+ end
110
+
111
+ # blacklisted ======================================================
112
+ def self.blacklisted_token_association(resource)
113
+ resource.class.blacklisted_token_association
114
+ end
115
+
116
+ def self.token_blacklisting_enabled?(resource)
117
+ blacklisted_token_association(resource).present?
118
+ end
119
+
120
+ def self.blacklisted_tokens_for(resource)
121
+ blacklisted_token_association = blacklisted_token_association(resource)
122
+ resource.send(blacklisted_token_association)
123
+ end
124
+
125
+ # Returns whether the JWT token is blacklisted or not
126
+ def self.blacklisted?(resource)
127
+ return false unless token_blacklisting_enabled?(resource)
128
+
129
+ blacklisted_tokens_for(resource).exists?(token: @token)
130
+ end
131
+
132
+ # Blacklist the current JWT token from future access
133
+ def self.blacklist_token
134
+
135
+ return unless token_blacklisting_enabled?(current_resource)
136
+ blacklisted_tokens_for(current_resource).create(token: @token, expire_at: Time.at(@decoded_token[:exp]).utc)
137
+ end
138
+
139
+
140
+ def self.method_missing(name, *args)
141
+ method_name = name.to_s
142
+
143
+ if method_name.start_with?('authenticate_and_set_')
144
+ resource_name = method_name.split('authenticate_and_set_')[1]
145
+ authenticate_and_set_resource(resource_name)
146
+ else
147
+ super
148
+ end
149
+ end
150
+
151
+ def self.respond_to_missing?(method_name, include_private = false)
152
+ method_name.to_s.start_with?('authenticate_and_set_') || super
153
+ end
154
+
155
+ # Authenticate the JWT token and set resource
156
+ def self.authenticate_and_set_resource(resource_name)
157
+ @resource_name = resource_name
158
+
159
+ @token = request.headers['Authorization']&.split('Bearer ')&.last
160
+ return render_error(401, message: I18n.t('api_guard.access_token.missing')) unless @token
161
+
162
+ authenticate_token
163
+
164
+ # Render error response only if no resource found and no previous render happened
165
+ render_error(401, message: I18n.t('api_guard.access_token.invalid')) if !current_resource && !performed?
166
+ rescue JWT::DecodeError => e
167
+ if e.message == 'Signature has expired'
168
+ render_error(401, message: I18n.t('api_guard.access_token.expired'))
169
+ else
170
+ render_error(401, message: I18n.t('api_guard.access_token.invalid'))
171
+ end
172
+ end
173
+
174
+ # Decode the JWT token
175
+ # and don't verify token expiry for refresh token API request
176
+ def self.decode_token
177
+ # TODO: Set token refresh controller dynamic
178
+ verify_token = (controller_name != 'tokens' || action_name != 'create')
179
+ @decoded_token = decode(@token, verify_token)
180
+ end
181
+
182
+ # Returns whether the JWT token is issued after the last password change
183
+ # Returns true if password hasn't changed by the user
184
+ def self.valid_issued_at?(resource)
185
+ return true unless ApiGuard.invalidate_old_tokens_on_password_change
186
+
187
+ !resource.token_issued_at || @decoded_token[:iat] >= resource.token_issued_at.to_i
188
+ end
189
+
190
+ # Defines "current_{{resource_name}}" method and "@current_{{resource_name}}" instance variable
191
+ # that returns "resource" value
192
+ def self.define_current_resource_accessors(resource)
193
+ define_singleton_method("current_#{@resource_name}") do
194
+ instance_variable_get("@current_#{@resource_name}") ||
195
+ instance_variable_set("@current_#{@resource_name}", resource)
196
+ end
197
+ end
198
+
199
+ # Authenticate the resource with the '{{resource_name}}_id' in the decoded JWT token
200
+ # and also, check for valid issued at time and not blacklisted
201
+ #
202
+ # Also, set "current_{{resource_name}}" method and "@current_{{resource_name}}" instance variable
203
+ # for accessing the authenticated resource
204
+ def self.authenticate_token
205
+ return unless decode_token
206
+
207
+ resource = find_resource_from_token(@resource_name.classify.constantize)
208
+
209
+ if resource && valid_issued_at?(resource) && !blacklisted?(resource)
210
+ define_current_resource_accessors(resource)
211
+ else
212
+ render_error(401, message: I18n.t('api_guard.access_token.invalid'))
213
+ end
214
+ end
215
+
216
+ def self.find_resource_from_token(resource_class)
217
+ resource_id = @decoded_token[:"#{@resource_name}_id"]
218
+ return if resource_id.blank?
219
+
220
+ resource_class.find_by(id: resource_id)
221
+ end
222
+
223
+ def self.current_resource
224
+ return unless respond_to?("current_#{@resource_name}")
225
+
226
+ public_send("current_#{@resource_name}")
227
+ end
70
228
  end
71
229
  end
72
230
  end
@@ -4,25 +4,25 @@ module ApiGuard
4
4
  module JwtAuth
5
5
  # Common module for refresh token functionality
6
6
  module RefreshJwtToken
7
- def refresh_token_association(resource)
7
+ def self.refresh_token_association(resource)
8
8
  resource.class.refresh_token_association
9
9
  end
10
10
 
11
- def refresh_token_enabled?(resource)
11
+ def self.refresh_token_enabled?(resource)
12
12
  refresh_token_association(resource).present?
13
13
  end
14
14
 
15
- def refresh_tokens_for(resource)
15
+ def self.refresh_tokens_for(resource)
16
16
  refresh_token_association = refresh_token_association(resource)
17
17
  resource.send(refresh_token_association)
18
18
  end
19
19
 
20
- def find_refresh_token_of(resource, refresh_token)
20
+ def self.find_refresh_token_of(resource, refresh_token)
21
21
  refresh_tokens_for(resource).find_by_token(refresh_token)
22
22
  end
23
23
 
24
24
  # Generate and return unique refresh token for the resource
25
- def uniq_refresh_token(resource)
25
+ def self.uniq_refresh_token(resource)
26
26
  loop do
27
27
  random_token = SecureRandom.urlsafe_base64
28
28
  return random_token unless refresh_tokens_for(resource).exists?(token: random_token)
@@ -30,17 +30,108 @@ module ApiGuard
30
30
  end
31
31
 
32
32
  # Create a new refresh_token for the current resource
33
- def new_refresh_token(resource)
33
+ def self.new_refresh_token(resource)
34
34
  return unless refresh_token_enabled?(resource)
35
35
 
36
36
  refresh_tokens_for(resource).create(token: uniq_refresh_token(resource)).token
37
37
  end
38
38
 
39
- def destroy_all_refresh_tokens(resource)
39
+ def self.destroy_all_refresh_tokens(resource)
40
40
  return unless refresh_token_enabled?(resource)
41
41
 
42
42
  refresh_tokens_for(resource).destroy_all
43
43
  end
44
+
45
+ # authenticat-----------
46
+
47
+ def self.method_missing(name, *args)
48
+ method_name = name.to_s
49
+
50
+ if method_name.start_with?('authenticate_and_set_')
51
+ resource_name = method_name.split('authenticate_and_set_')[1]
52
+ authenticate_and_set_resource(resource_name)
53
+ else
54
+ super
55
+ end
56
+ end
57
+
58
+ def self.respond_to_missing?(method_name, include_private = false)
59
+ method_name.to_s.start_with?('authenticate_and_set_') || super
60
+ end
61
+
62
+ # Authenticate the JWT token and set resource
63
+ def self.authenticate_and_set_resource(resource_name)
64
+ @resource_name = resource_name
65
+
66
+ @token = request.headers['Authorization']&.split('Bearer ')&.last
67
+ return render_error(401, message: I18n.t('api_guard.access_token.missing')) unless @token
68
+
69
+ authenticate_token
70
+
71
+ # Render error response only if no resource found and no previous render happened
72
+ render_error(401, message: I18n.t('api_guard.access_token.invalid')) if !current_resource && !performed?
73
+ rescue JWT::DecodeError => e
74
+ if e.message == 'Signature has expired'
75
+ render_error(401, message: I18n.t('api_guard.access_token.expired'))
76
+ else
77
+ render_error(401, message: I18n.t('api_guard.access_token.invalid'))
78
+ end
79
+ end
80
+
81
+ # Decode the JWT token
82
+ # and don't verify token expiry for refresh token API request
83
+ def self.decode_token
84
+ # TODO: Set token refresh controller dynamic
85
+ verify_token = (controller_name != 'tokens' || action_name != 'create')
86
+ @decoded_token = decode(@token, verify_token)
87
+ end
88
+
89
+ # Returns whether the JWT token is issued after the last password change
90
+ # Returns true if password hasn't changed by the user
91
+ def self.valid_issued_at?(resource)
92
+ return true unless ApiGuard.invalidate_old_tokens_on_password_change
93
+
94
+ !resource.token_issued_at || @decoded_token[:iat] >= resource.token_issued_at.to_i
95
+ end
96
+
97
+ # Defines "current_{{resource_name}}" method and "@current_{{resource_name}}" instance variable
98
+ # that returns "resource" value
99
+ def self.define_current_resource_accessors(resource)
100
+ define_singleton_method("current_#{@resource_name}") do
101
+ instance_variable_get("@current_#{@resource_name}") ||
102
+ instance_variable_set("@current_#{@resource_name}", resource)
103
+ end
104
+ end
105
+
106
+ # Authenticate the resource with the '{{resource_name}}_id' in the decoded JWT token
107
+ # and also, check for valid issued at time and not blacklisted
108
+ #
109
+ # Also, set "current_{{resource_name}}" method and "@current_{{resource_name}}" instance variable
110
+ # for accessing the authenticated resource
111
+ def self.authenticate_token
112
+ return unless decode_token
113
+
114
+ resource = find_resource_from_token(@resource_name.classify.constantize)
115
+
116
+ if resource && valid_issued_at?(resource) && !blacklisted?(resource)
117
+ define_current_resource_accessors(resource)
118
+ else
119
+ render_error(401, message: I18n.t('api_guard.access_token.invalid'))
120
+ end
121
+ end
122
+
123
+ def self.find_resource_from_token(resource_class)
124
+ resource_id = @decoded_token[:"#{@resource_name}_id"]
125
+ return if resource_id.blank?
126
+
127
+ resource_class.find_by(id: resource_id)
128
+ end
129
+
130
+ def self.current_resource
131
+ return unless respond_to?("current_#{@resource_name}")
132
+
133
+ public_send("current_#{@resource_name}")
134
+ end
44
135
  end
45
136
  end
46
137
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ApiGuard
4
- VERSION = '0.5.1'
4
+ VERSION = '0.5.6'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: api_guard_grape
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Prateek Singh
@@ -176,7 +176,7 @@ files:
176
176
  - lib/generators/api_guard/initializer/USAGE
177
177
  - lib/generators/api_guard/initializer/initializer_generator.rb
178
178
  - lib/generators/api_guard/initializer/templates/initializer.rb
179
- homepage: https://github.com/prateeksinghbundela/api_guard
179
+ homepage: https://github.com/prateeksinghbundela/api_guard_grape
180
180
  licenses:
181
181
  - MIT
182
182
  metadata: {}
@@ -195,7 +195,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
195
  - !ruby/object:Gem::Version
196
196
  version: '0'
197
197
  requirements: []
198
- rubygems_version: 3.0.3
198
+ rubygems_version: 3.1.4
199
199
  signing_key:
200
200
  specification_version: 4
201
201
  summary: Rails API authentication made easy