devise_token_auth 1.2.5 → 1.2.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 +4 -4
- data/README.md +1 -0
- data/app/controllers/devise_token_auth/passwords_controller.rb +2 -2
- data/app/controllers/devise_token_auth/sessions_controller.rb +1 -1
- data/app/models/devise_token_auth/concerns/confirmable_support.rb +1 -1
- data/app/models/devise_token_auth/concerns/user.rb +20 -13
- data/lib/devise_token_auth/version.rb +1 -1
- data/test/controllers/devise_token_auth/passwords_controller_test.rb +90 -29
- data/test/dummy/config/application.rb +1 -0
- data/test/dummy/config/environments/test.rb +6 -2
- data/test/models/user_test.rb +94 -0
- metadata +15 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2950da8c6b3eac0a2a3ee2b1659d5e321ca9890e5a9b0a1525dcbf11e95d67dd
|
|
4
|
+
data.tar.gz: d04830c1c12f69b3537e80fae17d5914b0b22272a52b78d7226a04be560cadea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a7e1dcce01dabc1e270926ff05b54c166d861232ef6ecf7547d88442e2d11ab7c104fef833312bf73a69dd5e3dd7df4923f050baf3e4aee877f32df6392d352
|
|
7
|
+
data.tar.gz: b44b9aa57a43fb58e86a7985957e9a5d425787ae65e898b480d7fefe02bf573433f27af356512ae2a4989c3a6d9f0b91a9cc1a1b3b2ef3f95a4b39557472ffa7
|
data/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Also, it maintains a session for each client/device, so you can have as many ses
|
|
|
23
23
|
* [redux-token-auth](https://github.com/kylecorbelli/redux-token-auth) for [React with Redux](https://github.com/reactjs/react-redux)
|
|
24
24
|
* [jToker](https://github.com/lynndylanhurley/j-toker) for [jQuery](https://jquery.com/)
|
|
25
25
|
* [vanilla-token-auth](https://github.com/theblang/vanilla-token-auth) for an unopinionated client
|
|
26
|
+
* [flutter_token_auth](https://github.com/diarmuidr3d/flutter_token_auth) for Flutter
|
|
26
27
|
* Oauth2 authentication using [OmniAuth](https://github.com/intridea/omniauth).
|
|
27
28
|
* Email authentication using [Devise](https://github.com/plataformatec/devise), including:
|
|
28
29
|
* User registration, update and deletion
|
|
@@ -56,7 +56,7 @@ module DeviseTokenAuth
|
|
|
56
56
|
set_token_in_cookie(@resource, token)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
redirect_header_options = { reset_password: true }
|
|
59
|
+
redirect_header_options = { reset_password: true, reset_password_token: resource_params[:reset_password_token] }
|
|
60
60
|
redirect_headers = build_redirect_headers(token.token,
|
|
61
61
|
token.client,
|
|
62
62
|
redirect_header_options)
|
|
@@ -73,7 +73,7 @@ module DeviseTokenAuth
|
|
|
73
73
|
# make sure user is authorized
|
|
74
74
|
if require_client_password_reset_token? && resource_params[:reset_password_token]
|
|
75
75
|
@resource = resource_class.with_reset_password_token(resource_params[:reset_password_token])
|
|
76
|
-
return render_update_error_unauthorized unless @resource
|
|
76
|
+
return render_update_error_unauthorized unless @resource && @resource.reset_password_period_valid?
|
|
77
77
|
|
|
78
78
|
@token = @resource.create_token
|
|
79
79
|
else
|
|
@@ -131,7 +131,7 @@ module DeviseTokenAuth
|
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def create_and_assign_token
|
|
134
|
-
if @resource.respond_to?(:with_lock)
|
|
134
|
+
if @resource.respond_to?(:with_lock) && !@resource.changed?
|
|
135
135
|
@resource.with_lock do
|
|
136
136
|
@token = @resource.create_token
|
|
137
137
|
@resource.save!
|
|
@@ -3,7 +3,7 @@ module DeviseTokenAuth::Concerns::ConfirmableSupport
|
|
|
3
3
|
|
|
4
4
|
included do
|
|
5
5
|
# Override standard devise `postpone_email_change?` method
|
|
6
|
-
# for not to use `
|
|
6
|
+
# for not to use `devise_will_save_change_to_email?` methods.
|
|
7
7
|
def postpone_email_change?
|
|
8
8
|
postpone = self.class.reconfirmable &&
|
|
9
9
|
email_value_in_database != email &&
|
|
@@ -41,8 +41,7 @@ module DeviseTokenAuth::Concerns::User
|
|
|
41
41
|
|
|
42
42
|
# don't use default devise email validation
|
|
43
43
|
def email_required?; false; end
|
|
44
|
-
def
|
|
45
|
-
def will_save_change_to_email?; false; end
|
|
44
|
+
def devise_will_save_change_to_email?; false; end
|
|
46
45
|
|
|
47
46
|
if DeviseTokenAuth.send_confirmation_email && devise_modules.include?(:confirmable)
|
|
48
47
|
include DeviseTokenAuth::Concerns::ConfirmableSupport
|
|
@@ -259,17 +258,25 @@ module DeviseTokenAuth::Concerns::User
|
|
|
259
258
|
end
|
|
260
259
|
|
|
261
260
|
def clean_old_tokens
|
|
262
|
-
if tokens.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
# Since the tokens are sorted by expiry, shift the oldest client token
|
|
271
|
-
# off the Hash until it no longer exceeds the maximum number of clients
|
|
272
|
-
tokens.shift while max_client_tokens_exceeded?
|
|
261
|
+
return if tokens.blank? || !max_client_tokens_exceeded?
|
|
262
|
+
|
|
263
|
+
# First, remove any tokens with expiry greater than current max allowed lifespan
|
|
264
|
+
# this handles the case where token lifespan was reduced and old tokens exist
|
|
265
|
+
max_lifespan_expiry = Time.now.to_i + DeviseTokenAuth.token_lifespan.to_i
|
|
266
|
+
tokens_to_keep = tokens.select do |_cid, v|
|
|
267
|
+
expiry = (v[:expiry] || v['expiry']).to_i
|
|
268
|
+
expiry <= max_lifespan_expiry
|
|
273
269
|
end
|
|
270
|
+
|
|
271
|
+
# Using Enumerable#sort_by on a Hash will typecast it into an associative
|
|
272
|
+
# Array (i.e. an Array of key-value Array pairs). However, since Hashes
|
|
273
|
+
# have an internal order in Ruby 1.9+, the resulting sorted associative
|
|
274
|
+
# Array can be converted back into a Hash, while maintaining the sorted
|
|
275
|
+
# order.
|
|
276
|
+
self.tokens = tokens_to_keep.sort_by { |_cid, v| v[:expiry] || v['expiry'] }.to_h
|
|
277
|
+
|
|
278
|
+
# Since the tokens are sorted by expiry, shift the oldest client token
|
|
279
|
+
# off the Hash until it no longer exceeds the maximum number of clients
|
|
280
|
+
tokens.shift while max_client_tokens_exceeded?
|
|
274
281
|
end
|
|
275
282
|
end
|
|
@@ -229,42 +229,73 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase
|
|
|
229
229
|
end
|
|
230
230
|
|
|
231
231
|
describe 'password reset link success' do
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
describe 'with require_client_password_reset_token is enabled' do
|
|
233
|
+
before do
|
|
234
|
+
DeviseTokenAuth.require_client_password_reset_token = true
|
|
235
|
+
get :edit,
|
|
236
|
+
params: { reset_password_token: @mail_reset_token,
|
|
237
|
+
redirect_url: @mail_redirect_url }
|
|
236
238
|
|
|
237
|
-
|
|
239
|
+
@resource.reload
|
|
238
240
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
raw_qs = response.location.split('?')[1]
|
|
242
|
+
@qs = Rack::Utils.parse_nested_query(raw_qs)
|
|
241
243
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
@client = @qs['client']
|
|
245
|
-
@expiry = @qs['expiry']
|
|
246
|
-
@reset_password = @qs['reset_password']
|
|
247
|
-
@token = @qs['token']
|
|
248
|
-
@uid = @qs['uid']
|
|
249
|
-
end
|
|
244
|
+
@reset_password_token = @qs['reset_password_token']
|
|
245
|
+
end
|
|
250
246
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
247
|
+
test 'response should have success redirect status' do
|
|
248
|
+
assert_equal 302, response.status
|
|
249
|
+
end
|
|
254
250
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
assert @client_id
|
|
259
|
-
assert @expiry
|
|
260
|
-
assert @reset_password
|
|
261
|
-
assert @token
|
|
262
|
-
assert @uid
|
|
251
|
+
test 'response should contain reset_password_token param' do
|
|
252
|
+
assert_equal @mail_reset_token, @qs['reset_password_token']
|
|
253
|
+
end
|
|
263
254
|
end
|
|
264
255
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
256
|
+
describe 'require_client_password_reset_token is disabled' do
|
|
257
|
+
before do
|
|
258
|
+
DeviseTokenAuth.require_client_password_reset_token = false
|
|
259
|
+
get :edit,
|
|
260
|
+
params: { reset_password_token: @mail_reset_token,
|
|
261
|
+
redirect_url: @mail_redirect_url }
|
|
262
|
+
|
|
263
|
+
@resource.reload
|
|
264
|
+
|
|
265
|
+
raw_qs = response.location.split('?')[1]
|
|
266
|
+
@qs = Rack::Utils.parse_nested_query(raw_qs)
|
|
267
|
+
|
|
268
|
+
@access_token = @qs['access-token']
|
|
269
|
+
@client_id = @qs['client_id']
|
|
270
|
+
@client = @qs['client']
|
|
271
|
+
@expiry = @qs['expiry']
|
|
272
|
+
@reset_password = @qs['reset_password']
|
|
273
|
+
@token = @qs['token']
|
|
274
|
+
@uid = @qs['uid']
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
test 'response should have success redirect status' do
|
|
278
|
+
assert_equal 302, response.status
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
test 'response should contain auth params' do
|
|
282
|
+
assert @access_token
|
|
283
|
+
assert @client
|
|
284
|
+
assert @client_id
|
|
285
|
+
assert @expiry
|
|
286
|
+
assert @reset_password
|
|
287
|
+
assert @token
|
|
288
|
+
assert @uid
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
test 'response auth params should be valid' do
|
|
292
|
+
assert @resource.valid_token?(@token, @client_id)
|
|
293
|
+
assert @resource.valid_token?(@access_token, @client)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
test 'response should contain reset_password_token param' do
|
|
297
|
+
assert_equal @mail_reset_token, @qs['reset_password_token']
|
|
298
|
+
end
|
|
268
299
|
end
|
|
269
300
|
end
|
|
270
301
|
end
|
|
@@ -715,6 +746,36 @@ class DeviseTokenAuth::PasswordsControllerTest < ActionController::TestCase
|
|
|
715
746
|
end
|
|
716
747
|
end
|
|
717
748
|
|
|
749
|
+
describe 'with expired reset password token' do
|
|
750
|
+
before do
|
|
751
|
+
DeviseTokenAuth.require_client_password_reset_token = true
|
|
752
|
+
reset_password_token = @resource.send_reset_password_instructions
|
|
753
|
+
@resource.update! reset_password_sent_at: 2.days.ago
|
|
754
|
+
|
|
755
|
+
@new_password = Faker::Internet.password
|
|
756
|
+
@params = { password: @new_password,
|
|
757
|
+
password_confirmation: @new_password,
|
|
758
|
+
reset_password_token: reset_password_token }
|
|
759
|
+
|
|
760
|
+
put :update, params: @params
|
|
761
|
+
|
|
762
|
+
@data = JSON.parse(response.body)
|
|
763
|
+
@resource.reload
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
test 'request should fail' do
|
|
767
|
+
assert_equal 401, response.status
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
test 'new password should not authenticate user' do
|
|
771
|
+
assert !@resource.valid_password?(@new_password)
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
teardown do
|
|
775
|
+
DeviseTokenAuth.require_client_password_reset_token = false
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
|
|
718
779
|
describe 'with invalid reset password token' do
|
|
719
780
|
before do
|
|
720
781
|
DeviseTokenAuth.require_client_password_reset_token = true
|
|
@@ -23,7 +23,7 @@ Rails.application.configure do
|
|
|
23
23
|
(config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' }) :
|
|
24
24
|
(config.static_cache_control = 'public, max-age=3600')
|
|
25
25
|
|
|
26
|
-
if Rails::VERSION::MAJOR
|
|
26
|
+
if Rails::VERSION::MAJOR < 7 && ENV['DEVISE_TOKEN_AUTH_ORM'] != 'mongoid'
|
|
27
27
|
config.active_record.legacy_connection_handling = false
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -32,7 +32,11 @@ Rails.application.configure do
|
|
|
32
32
|
config.action_controller.perform_caching = false
|
|
33
33
|
|
|
34
34
|
# Raise exceptions instead of rendering exception templates.
|
|
35
|
-
|
|
35
|
+
if Rails::VERSION::MAJOR >= 7 && Rails::VERSION::MINOR > 0
|
|
36
|
+
config.action_dispatch.show_exceptions = :none
|
|
37
|
+
else
|
|
38
|
+
config.action_dispatch.show_exceptions = false
|
|
39
|
+
end
|
|
36
40
|
|
|
37
41
|
# Disable request forgery protection in test environment.
|
|
38
42
|
config.action_controller.allow_forgery_protection = false
|
data/test/models/user_test.rb
CHANGED
|
@@ -127,4 +127,98 @@ class UserTest < ActiveSupport::TestCase
|
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
end
|
|
130
|
+
|
|
131
|
+
describe 'clean_old_tokens' do
|
|
132
|
+
before do
|
|
133
|
+
@resource = create(:user, :confirmed)
|
|
134
|
+
@token_lifespan = DeviseTokenAuth.token_lifespan
|
|
135
|
+
@max_client_count = DeviseTokenAuth.max_number_of_devices
|
|
136
|
+
DeviseTokenAuth.max_number_of_devices = 2
|
|
137
|
+
DeviseTokenAuth.token_lifespan = 1.week
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
after do
|
|
141
|
+
DeviseTokenAuth.token_lifespan = @token_lifespan
|
|
142
|
+
DeviseTokenAuth.max_number_of_devices = @max_client_count
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
test 'removes tokens with expiry beyond the maximum lifespan' do
|
|
146
|
+
# Create tokens with different expiry times
|
|
147
|
+
current_time = Time.now.to_i
|
|
148
|
+
|
|
149
|
+
max_lifespan = current_time + DeviseTokenAuth.token_lifespan.to_i
|
|
150
|
+
|
|
151
|
+
# Valid token within lifespan
|
|
152
|
+
@resource.tokens['valid_client'] = {
|
|
153
|
+
'token' => 'valid_token',
|
|
154
|
+
'expiry' => current_time + 1.day.to_i
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Token exactly at max lifespan (should be kept)
|
|
158
|
+
@resource.tokens['edge_client'] = {
|
|
159
|
+
'token' => 'edge_token',
|
|
160
|
+
'expiry' => max_lifespan
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Token beyond max lifespan (should be removed)
|
|
164
|
+
@resource.tokens['expired_client'] = {
|
|
165
|
+
'token' => 'expired_token',
|
|
166
|
+
'expiry' => max_lifespan + 1.day.to_i
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Call the method under test
|
|
170
|
+
@resource.send(:clean_old_tokens)
|
|
171
|
+
|
|
172
|
+
# Assert that tokens beyond lifespan were removed
|
|
173
|
+
assert @resource.tokens.key?('valid_client'), 'Valid token should be kept'
|
|
174
|
+
assert @resource.tokens.key?('edge_client'), 'Edge case token at max lifespan should be kept'
|
|
175
|
+
refute @resource.tokens.key?('expired_client'), 'Token beyond max lifespan should be removed'
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
test 'handles token lifespan reduction when creating token' do
|
|
179
|
+
# Setup: Create the maximum allowed number of tokens with a longer lifespan
|
|
180
|
+
DeviseTokenAuth.token_lifespan = 2.weeks
|
|
181
|
+
DeviseTokenAuth.max_number_of_devices = 3
|
|
182
|
+
|
|
183
|
+
# Create tokens at different times but all within the initial long lifespan
|
|
184
|
+
@resource.tokens = {}
|
|
185
|
+
@resource.tokens['client_1'] = {
|
|
186
|
+
'token' => 'token_1',
|
|
187
|
+
'expiry' => Time.now.to_i + 12.days.to_i
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@resource.tokens['client_2'] = {
|
|
191
|
+
'token' => 'token_2',
|
|
192
|
+
'expiry' => Time.now.to_i + 10.days.to_i
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@resource.tokens['client_3'] = {
|
|
196
|
+
'token' => 'token_3',
|
|
197
|
+
'expiry' => Time.now.to_i + 5.days.to_i
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# We've reached the maximum number of devices/tokens
|
|
201
|
+
assert_equal 3, @resource.tokens.length
|
|
202
|
+
|
|
203
|
+
# Now reduce token lifespan - simulating a config change
|
|
204
|
+
DeviseTokenAuth.token_lifespan = 1.week
|
|
205
|
+
|
|
206
|
+
# Create a new token which should trigger clean_old_tokens
|
|
207
|
+
new_auth_headers = @resource.create_new_auth_token
|
|
208
|
+
new_client = new_auth_headers['client']
|
|
209
|
+
|
|
210
|
+
# The new token should exist
|
|
211
|
+
assert @resource.tokens.key?(new_client), 'New token should exist'
|
|
212
|
+
|
|
213
|
+
# Tokens exceeding the new reduced lifespan should be removed
|
|
214
|
+
refute @resource.tokens.key?('client_1'), 'Token with expiry > new lifespan should be removed'
|
|
215
|
+
refute @resource.tokens.key?('client_2'), 'Token with expiry > new lifespan should be removed'
|
|
216
|
+
|
|
217
|
+
# Token within new lifespan should be kept
|
|
218
|
+
assert @resource.tokens.key?('client_3'), 'Token within new reduced lifespan should be kept'
|
|
219
|
+
|
|
220
|
+
# We should have exactly 2 tokens: the new one and client_3
|
|
221
|
+
assert_equal 2, @resource.tokens.length
|
|
222
|
+
end
|
|
223
|
+
end
|
|
130
224
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: devise_token_auth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lynn Hurley
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-11-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -19,7 +19,7 @@ dependencies:
|
|
|
19
19
|
version: 4.2.0
|
|
20
20
|
- - "<"
|
|
21
21
|
- !ruby/object:Gem::Version
|
|
22
|
-
version: '8.
|
|
22
|
+
version: '8.2'
|
|
23
23
|
type: :runtime
|
|
24
24
|
prerelease: false
|
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -29,7 +29,7 @@ dependencies:
|
|
|
29
29
|
version: 4.2.0
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '8.
|
|
32
|
+
version: '8.2'
|
|
33
33
|
- !ruby/object:Gem::Dependency
|
|
34
34
|
name: devise
|
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -144,16 +144,22 @@ dependencies:
|
|
|
144
144
|
name: mongoid-locker
|
|
145
145
|
requirement: !ruby/object:Gem::Requirement
|
|
146
146
|
requirements:
|
|
147
|
-
- - "
|
|
147
|
+
- - ">="
|
|
148
148
|
- !ruby/object:Gem::Version
|
|
149
|
-
version: '
|
|
149
|
+
version: '1.0'
|
|
150
|
+
- - "<"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '3.0'
|
|
150
153
|
type: :development
|
|
151
154
|
prerelease: false
|
|
152
155
|
version_requirements: !ruby/object:Gem::Requirement
|
|
153
156
|
requirements:
|
|
154
|
-
- - "
|
|
157
|
+
- - ">="
|
|
155
158
|
- !ruby/object:Gem::Version
|
|
156
|
-
version: '
|
|
159
|
+
version: '1.0'
|
|
160
|
+
- - "<"
|
|
161
|
+
- !ruby/object:Gem::Version
|
|
162
|
+
version: '3.0'
|
|
157
163
|
description: For use with client side single page apps such as the venerable https://github.com/lynndylanhurley/ng-token-auth.
|
|
158
164
|
email:
|
|
159
165
|
- lynn.dylan.hurley@gmail.com
|
|
@@ -358,7 +364,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
358
364
|
- !ruby/object:Gem::Version
|
|
359
365
|
version: '0'
|
|
360
366
|
requirements: []
|
|
361
|
-
rubygems_version: 3.
|
|
367
|
+
rubygems_version: 3.4.19
|
|
362
368
|
signing_key:
|
|
363
369
|
specification_version: 4
|
|
364
370
|
summary: Token based authentication for rails. Uses Devise + OmniAuth.
|