devise-tokens 1.0.0 → 1.0.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.
- checksums.yaml +4 -4
- data/app/controllers/devise_tokens/application_controller.rb +77 -0
- data/app/controllers/devise_tokens/concerns/resource_finder.rb +42 -0
- data/app/controllers/devise_tokens/concerns/set_user_by_token.rb +160 -0
- data/app/controllers/devise_tokens/confirmations_controller.rb +79 -0
- data/app/controllers/devise_tokens/omniauth_callbacks_controller.rb +284 -0
- data/app/controllers/devise_tokens/passwords_controller.rb +204 -0
- data/app/controllers/devise_tokens/registrations_controller.rb +203 -0
- data/app/controllers/devise_tokens/sessions_controller.rb +128 -0
- data/app/controllers/devise_tokens/token_validations_controller.rb +29 -0
- data/app/controllers/devise_tokens/unlocks_controller.rb +87 -0
- data/app/models/devise_token_auth/concerns/active_record_support.rb +16 -0
- data/app/models/devise_token_auth/concerns/mongoid_support.rb +19 -0
- data/app/models/devise_token_auth/concerns/tokens_serialization.rb +19 -0
- data/app/models/devise_token_auth/concerns/user.rb +253 -0
- data/app/models/devise_token_auth/concerns/user_omniauth_callbacks.rb +28 -0
- data/app/validators/devise_token_auth_email_validator.rb +23 -0
- data/app/views/devise/mailer/confirmation_instructions.html.erb +5 -0
- data/app/views/devise/mailer/reset_password_instructions.html.erb +8 -0
- data/app/views/devise/mailer/unlock_instructions.html.erb +7 -0
- data/app/views/devise_token_auth/omniauth_external_window.html.erb +38 -0
- data/config/locales/da-DK.yml +52 -0
- data/config/locales/de.yml +51 -0
- data/config/locales/en.yml +57 -0
- data/config/locales/es.yml +51 -0
- data/config/locales/fr.yml +51 -0
- data/config/locales/he.yml +52 -0
- data/config/locales/it.yml +48 -0
- data/config/locales/ja.yml +48 -0
- data/config/locales/nl.yml +32 -0
- data/config/locales/pl.yml +50 -0
- data/config/locales/pt-BR.yml +48 -0
- data/config/locales/pt.yml +50 -0
- data/config/locales/ro.yml +48 -0
- data/config/locales/ru.yml +52 -0
- data/config/locales/sq.yml +48 -0
- data/config/locales/sv.yml +52 -0
- data/config/locales/uk.yml +61 -0
- data/config/locales/vi.yml +52 -0
- data/config/locales/zh-CN.yml +48 -0
- data/config/locales/zh-HK.yml +50 -0
- data/config/locales/zh-TW.yml +50 -0
- data/lib/devise_tokens.rb +14 -0
- data/lib/devise_tokens/blacklist.rb +2 -0
- data/lib/devise_tokens/controllers/helpers.rb +161 -0
- data/lib/devise_tokens/controllers/url_helpers.rb +10 -0
- data/lib/devise_tokens/engine.rb +92 -0
- data/lib/devise_tokens/errors.rb +6 -0
- data/lib/devise_tokens/rails/routes.rb +116 -0
- data/lib/devise_tokens/token_factory.rb +126 -0
- data/lib/devise_tokens/url.rb +39 -0
- data/lib/devise_tokens/version.rb +3 -0
- data/lib/generators/devise_tokens/USAGE +31 -0
- data/lib/generators/devise_tokens/install_generator.rb +91 -0
- data/lib/generators/devise_tokens/install_generator_helpers.rb +98 -0
- data/lib/generators/devise_tokens/install_mongoid_generator.rb +46 -0
- data/lib/generators/devise_tokens/install_views_generator.rb +18 -0
- data/lib/generators/devise_tokens/templates/devise_tokens.rb +55 -0
- data/lib/generators/devise_tokens/templates/devise_tokens_create_users.rb.erb +49 -0
- data/lib/generators/devise_tokens/templates/user.rb.erb +9 -0
- data/lib/generators/devise_tokens/templates/user_mongoid.rb.erb +56 -0
- data/lib/tasks/devise_tokens_tasks.rake +6 -0
- metadata +208 -4
- data/lib/devise-tokens.rb +0 -5
@@ -0,0 +1,29 @@
|
|
1
|
+
module DeviseTokens
|
2
|
+
class TokenValidationsController < DeviseTokens::ApplicationController
|
3
|
+
skip_before_action :assert_is_devise_resource!, only: [:validate_token]
|
4
|
+
before_action :set_user_by_token, only: [:validate_token]
|
5
|
+
|
6
|
+
def validate_token
|
7
|
+
# @resource will have been set by set_user_by_token concern
|
8
|
+
if @resource
|
9
|
+
yield @resource if block_given?
|
10
|
+
render_validate_token_success
|
11
|
+
else
|
12
|
+
render_validate_token_error
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def render_validate_token_success
|
19
|
+
render json: {
|
20
|
+
success: true,
|
21
|
+
data: resource_data(resource_json: @resource.token_validation_response)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def render_validate_token_error
|
26
|
+
render_error(401, I18n.t('devise_tokens.token_validations.invalid'))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module DeviseTokens
|
2
|
+
class UnlocksController < DeviseTokens::ApplicationController
|
3
|
+
skip_after_action :update_auth_header, only: [:create, :show]
|
4
|
+
|
5
|
+
# this action is responsible for generating unlock tokens and
|
6
|
+
# sending emails
|
7
|
+
def create
|
8
|
+
return render_create_error_missing_email unless resource_params[:email]
|
9
|
+
|
10
|
+
@email = get_case_insensitive_field_from_resource_params(:email)
|
11
|
+
@resource = find_resource(:email, @email)
|
12
|
+
|
13
|
+
if @resource
|
14
|
+
yield @resource if block_given?
|
15
|
+
|
16
|
+
@resource.send_unlock_instructions(
|
17
|
+
email: @email,
|
18
|
+
provider: 'email',
|
19
|
+
client_config: params[:config_name]
|
20
|
+
)
|
21
|
+
|
22
|
+
if @resource.errors.empty?
|
23
|
+
return render_create_success
|
24
|
+
else
|
25
|
+
render_create_error @resource.errors
|
26
|
+
end
|
27
|
+
else
|
28
|
+
render_not_found_error
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def show
|
33
|
+
@resource = resource_class.unlock_access_by_token(params[:unlock_token])
|
34
|
+
|
35
|
+
if @resource.persisted?
|
36
|
+
token = @resource.create_token
|
37
|
+
@resource.save!
|
38
|
+
yield @resource if block_given?
|
39
|
+
|
40
|
+
redirect_header_options = { unlock: true }
|
41
|
+
redirect_headers = build_redirect_headers(token.token,
|
42
|
+
token.client,
|
43
|
+
redirect_header_options)
|
44
|
+
redirect_to(@resource.build_auth_url(after_unlock_path_for(@resource),
|
45
|
+
redirect_headers))
|
46
|
+
else
|
47
|
+
render_show_error
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def after_unlock_path_for(resource)
|
53
|
+
#TODO: This should probably be a configuration option at the very least.
|
54
|
+
'/'
|
55
|
+
end
|
56
|
+
|
57
|
+
def render_create_error_missing_email
|
58
|
+
render_error(401, I18n.t('devise_tokens.unlocks.missing_email'))
|
59
|
+
end
|
60
|
+
|
61
|
+
def render_create_success
|
62
|
+
render json: {
|
63
|
+
success: true,
|
64
|
+
message: I18n.t('devise_tokens.unlocks.sended', email: @email)
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def render_create_error(errors)
|
69
|
+
render json: {
|
70
|
+
success: false,
|
71
|
+
errors: errors
|
72
|
+
}, status: 400
|
73
|
+
end
|
74
|
+
|
75
|
+
def render_show_error
|
76
|
+
raise ActionController::RoutingError, 'Not Found'
|
77
|
+
end
|
78
|
+
|
79
|
+
def render_not_found_error
|
80
|
+
render_error(404, I18n.t('devise_tokens.unlocks.user_not_found', email: @email))
|
81
|
+
end
|
82
|
+
|
83
|
+
def resource_params
|
84
|
+
params.permit(:email, :unlock_token, :config)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'tokens_serialization'
|
2
|
+
|
3
|
+
module DeviseTokens::Concerns::ActiveRecordSupport
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
serialize :tokens, DeviseTokens::Concerns::TokensSerialization
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
# It's abstract replacement .find_by
|
12
|
+
def dta_find_by(attrs = {})
|
13
|
+
find_by(attrs)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DeviseTokens::Concerns::MongoidSupport
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def as_json(options = {})
|
5
|
+
options[:except] = (options[:except] || []) + [:_id]
|
6
|
+
hash = super(options)
|
7
|
+
hash['id'] = to_param
|
8
|
+
hash
|
9
|
+
end
|
10
|
+
|
11
|
+
class_methods do
|
12
|
+
# It's abstract replacement .find_by
|
13
|
+
def dta_find_by(attrs = {})
|
14
|
+
find_by(attrs)
|
15
|
+
rescue Mongoid::Errors::DocumentNotFound
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DeviseTokens::Concerns::TokensSerialization
|
2
|
+
# Serialization hash to json
|
3
|
+
def self.dump(object)
|
4
|
+
object.each_value(&:compact!) unless object.nil?
|
5
|
+
JSON.generate(object)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Deserialization json to hash
|
9
|
+
def self.load(json)
|
10
|
+
case json
|
11
|
+
when String
|
12
|
+
JSON.parse(json)
|
13
|
+
when NilClass
|
14
|
+
{}
|
15
|
+
else
|
16
|
+
json
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module DeviseTokens::Concerns::User
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def self.tokens_match?(token_hash, token)
|
7
|
+
@token_equality_cache ||= {}
|
8
|
+
|
9
|
+
key = "#{token_hash}/#{token}"
|
10
|
+
result = @token_equality_cache[key] ||= DeviseTokens::TokenFactory.token_hash_is_token?(token_hash, token)
|
11
|
+
@token_equality_cache = {} if @token_equality_cache.size > 10000
|
12
|
+
result
|
13
|
+
end
|
14
|
+
|
15
|
+
included do
|
16
|
+
# Hack to check if devise is already enabled
|
17
|
+
if method_defined?(:devise_modules)
|
18
|
+
devise_modules.delete(:omniauthable)
|
19
|
+
else
|
20
|
+
devise :database_authenticatable, :registerable,
|
21
|
+
:recoverable, :validatable, :confirmable
|
22
|
+
end
|
23
|
+
|
24
|
+
if const_defined?('ActiveRecord') && ancestors.include?(ActiveRecord::Base)
|
25
|
+
include DeviseTokens::Concerns::ActiveRecordSupport
|
26
|
+
end
|
27
|
+
|
28
|
+
if const_defined?('Mongoid') && ancestors.include?(Mongoid::Document)
|
29
|
+
include DeviseTokens::Concerns::MongoidSupport
|
30
|
+
end
|
31
|
+
|
32
|
+
if DeviseTokens.default_callbacks
|
33
|
+
include DeviseTokens::Concerns::UserOmniauthCallbacks
|
34
|
+
end
|
35
|
+
|
36
|
+
# get rid of dead tokens
|
37
|
+
before_save :destroy_expired_tokens
|
38
|
+
|
39
|
+
# remove old tokens if password has changed
|
40
|
+
before_save :remove_tokens_after_password_reset
|
41
|
+
|
42
|
+
# don't use default devise email validation
|
43
|
+
def email_required?; false; end
|
44
|
+
def email_changed?; false; end
|
45
|
+
def will_save_change_to_email?; false; end
|
46
|
+
|
47
|
+
def password_required?
|
48
|
+
return false unless provider == 'email'
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
# override devise method to include additional info as opts hash
|
53
|
+
def send_confirmation_instructions(opts = {})
|
54
|
+
generate_confirmation_token! unless @raw_confirmation_token
|
55
|
+
|
56
|
+
# fall back to "default" config name
|
57
|
+
opts[:client_config] ||= 'default'
|
58
|
+
opts[:to] = unconfirmed_email if pending_reconfirmation?
|
59
|
+
opts[:redirect_url] ||= DeviseTokens.default_confirm_success_url
|
60
|
+
|
61
|
+
send_devise_notification(:confirmation_instructions, @raw_confirmation_token, opts)
|
62
|
+
end
|
63
|
+
|
64
|
+
# override devise method to include additional info as opts hash
|
65
|
+
def send_reset_password_instructions(opts = {})
|
66
|
+
token = set_reset_password_token
|
67
|
+
|
68
|
+
# fall back to "default" config name
|
69
|
+
opts[:client_config] ||= 'default'
|
70
|
+
|
71
|
+
send_devise_notification(:reset_password_instructions, token, opts)
|
72
|
+
token
|
73
|
+
end
|
74
|
+
|
75
|
+
# override devise method to include additional info as opts hash
|
76
|
+
def send_unlock_instructions(opts = {})
|
77
|
+
raw, enc = Devise.token_generator.generate(self.class, :unlock_token)
|
78
|
+
self.unlock_token = enc
|
79
|
+
save(validate: false)
|
80
|
+
|
81
|
+
# fall back to "default" config name
|
82
|
+
opts[:client_config] ||= 'default'
|
83
|
+
|
84
|
+
send_devise_notification(:unlock_instructions, raw, opts)
|
85
|
+
raw
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_token(client: nil, lifespan: nil, cost: nil, **token_extras)
|
89
|
+
token = DeviseTokens::TokenFactory.create(client: client, lifespan: lifespan, cost: cost)
|
90
|
+
|
91
|
+
tokens[token.client] = {
|
92
|
+
token: token.token_hash,
|
93
|
+
expiry: token.expiry
|
94
|
+
}.merge!(token_extras)
|
95
|
+
|
96
|
+
clean_old_tokens
|
97
|
+
|
98
|
+
token
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def valid_token?(token, client = 'default')
|
103
|
+
return false unless tokens[client]
|
104
|
+
return true if token_is_current?(token, client)
|
105
|
+
return true if token_can_be_reused?(token, client)
|
106
|
+
|
107
|
+
# return false if none of the above conditions are met
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
# this must be done from the controller so that additional params
|
112
|
+
# can be passed on from the client
|
113
|
+
def send_confirmation_notification?; false; end
|
114
|
+
|
115
|
+
def token_is_current?(token, client)
|
116
|
+
# ghetto HashWithIndifferentAccess
|
117
|
+
expiry = tokens[client]['expiry'] || tokens[client][:expiry]
|
118
|
+
token_hash = tokens[client]['token'] || tokens[client][:token]
|
119
|
+
|
120
|
+
return true if (
|
121
|
+
# ensure that expiry and token are set
|
122
|
+
expiry && token &&
|
123
|
+
|
124
|
+
# ensure that the token has not yet expired
|
125
|
+
DateTime.strptime(expiry.to_s, '%s') > Time.zone.now &&
|
126
|
+
|
127
|
+
# ensure that the token is valid
|
128
|
+
DeviseTokens::Concerns::User.tokens_match?(token_hash, token)
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
# allow batch requests to use the previous token
|
133
|
+
def token_can_be_reused?(token, client)
|
134
|
+
# ghetto HashWithIndifferentAccess
|
135
|
+
updated_at = tokens[client]['updated_at'] || tokens[client][:updated_at]
|
136
|
+
last_token = tokens[client]['last_token'] || tokens[client][:last_token]
|
137
|
+
|
138
|
+
return true if (
|
139
|
+
# ensure that the last token and its creation time exist
|
140
|
+
updated_at && last_token &&
|
141
|
+
|
142
|
+
# ensure that previous token falls within the batch buffer throttle time of the last request
|
143
|
+
updated_at.to_time > Time.zone.now - DeviseTokens.batch_request_buffer_throttle &&
|
144
|
+
|
145
|
+
# ensure that the token is valid
|
146
|
+
DeviseTokens::TokenFactory.valid_token_hash?(last_token)
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
150
|
+
# update user's auth token (should happen on each request)
|
151
|
+
def create_new_auth_token(client = nil)
|
152
|
+
now = Time.zone.now
|
153
|
+
|
154
|
+
token = create_token(
|
155
|
+
client: client,
|
156
|
+
last_token: tokens.fetch(client, {})['token'],
|
157
|
+
updated_at: now
|
158
|
+
)
|
159
|
+
|
160
|
+
update_auth_header(token.token, token.client)
|
161
|
+
end
|
162
|
+
|
163
|
+
def build_auth_header(token, client = 'default')
|
164
|
+
# client may use expiry to prevent validation request if expired
|
165
|
+
# must be cast as string or headers will break
|
166
|
+
expiry = tokens[client]['expiry'] || tokens[client][:expiry]
|
167
|
+
|
168
|
+
{
|
169
|
+
DeviseTokens.headers_names[:"access-token"] => token,
|
170
|
+
DeviseTokens.headers_names[:"token-type"] => 'Bearer',
|
171
|
+
DeviseTokens.headers_names[:"client"] => client,
|
172
|
+
DeviseTokens.headers_names[:"expiry"] => expiry.to_s,
|
173
|
+
DeviseTokens.headers_names[:"uid"] => uid
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
def update_auth_header(token, client = 'default')
|
178
|
+
headers = build_auth_header(token, client)
|
179
|
+
clean_old_tokens
|
180
|
+
save!
|
181
|
+
|
182
|
+
headers
|
183
|
+
end
|
184
|
+
|
185
|
+
def build_auth_url(base_url, args)
|
186
|
+
args[:uid] = uid
|
187
|
+
args[:expiry] = tokens[args[:client_id]]['expiry']
|
188
|
+
|
189
|
+
DeviseTokens::Url.generate(base_url, args)
|
190
|
+
end
|
191
|
+
|
192
|
+
def extend_batch_buffer(token, client)
|
193
|
+
tokens[client]['updated_at'] = Time.zone.now
|
194
|
+
update_auth_header(token, client)
|
195
|
+
end
|
196
|
+
|
197
|
+
def confirmed?
|
198
|
+
devise_modules.exclude?(:confirmable) || super
|
199
|
+
end
|
200
|
+
|
201
|
+
def token_validation_response
|
202
|
+
as_json(except: %i[tokens created_at updated_at])
|
203
|
+
end
|
204
|
+
|
205
|
+
protected
|
206
|
+
|
207
|
+
def destroy_expired_tokens
|
208
|
+
if tokens
|
209
|
+
tokens.delete_if do |cid, v|
|
210
|
+
expiry = v[:expiry] || v['expiry']
|
211
|
+
DateTime.strptime(expiry.to_s, '%s') < Time.zone.now
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def should_remove_tokens_after_password_reset?
|
217
|
+
if Rails::VERSION::MAJOR <= 5
|
218
|
+
encrypted_password_changed? &&
|
219
|
+
DeviseTokens.remove_tokens_after_password_reset
|
220
|
+
else
|
221
|
+
saved_change_to_attribute?(:encrypted_password) &&
|
222
|
+
DeviseTokens.remove_tokens_after_password_reset
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def remove_tokens_after_password_reset
|
227
|
+
return unless should_remove_tokens_after_password_reset?
|
228
|
+
|
229
|
+
if tokens.present? && tokens.many?
|
230
|
+
client, token_data = tokens.max_by { |cid, v| v[:expiry] || v['expiry'] }
|
231
|
+
self.tokens = { client => token_data }
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def max_client_tokens_exceeded?
|
236
|
+
tokens.length > DeviseTokens.max_number_of_devices
|
237
|
+
end
|
238
|
+
|
239
|
+
def clean_old_tokens
|
240
|
+
if tokens.present? && max_client_tokens_exceeded?
|
241
|
+
# Using Enumerable#sort_by on a Hash will typecast it into an associative
|
242
|
+
# Array (i.e. an Array of key-value Array pairs). However, since Hashes
|
243
|
+
# have an internal order in Ruby 1.9+, the resulting sorted associative
|
244
|
+
# Array can be converted back into a Hash, while maintaining the sorted
|
245
|
+
# order.
|
246
|
+
self.tokens = tokens.sort_by { |_cid, v| v[:expiry] || v['expiry'] }.to_h
|
247
|
+
|
248
|
+
# Since the tokens are sorted by expiry, shift the oldest client token
|
249
|
+
# off the Hash until it no longer exceeds the maximum number of clients
|
250
|
+
tokens.shift while max_client_tokens_exceeded?
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module DeviseTokens::Concerns::UserOmniauthCallbacks
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
validates :email, presence: true,if: :email_provider?
|
8
|
+
validates :email, :devise_tokens_email => true, allow_nil: true, allow_blank: true, if: :email_provider?
|
9
|
+
validates_presence_of :uid, unless: :email_provider?
|
10
|
+
|
11
|
+
# only validate unique emails among email registration users
|
12
|
+
validates :email, uniqueness: { scope: :provider }, on: :create, if: :email_provider?
|
13
|
+
|
14
|
+
# keep uid in sync with email
|
15
|
+
before_save :sync_uid
|
16
|
+
before_create :sync_uid
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def email_provider?
|
22
|
+
provider == 'email'
|
23
|
+
end
|
24
|
+
|
25
|
+
def sync_uid
|
26
|
+
self.uid = email if email_provider?
|
27
|
+
end
|
28
|
+
end
|