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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/devise_tokens/application_controller.rb +77 -0
  3. data/app/controllers/devise_tokens/concerns/resource_finder.rb +42 -0
  4. data/app/controllers/devise_tokens/concerns/set_user_by_token.rb +160 -0
  5. data/app/controllers/devise_tokens/confirmations_controller.rb +79 -0
  6. data/app/controllers/devise_tokens/omniauth_callbacks_controller.rb +284 -0
  7. data/app/controllers/devise_tokens/passwords_controller.rb +204 -0
  8. data/app/controllers/devise_tokens/registrations_controller.rb +203 -0
  9. data/app/controllers/devise_tokens/sessions_controller.rb +128 -0
  10. data/app/controllers/devise_tokens/token_validations_controller.rb +29 -0
  11. data/app/controllers/devise_tokens/unlocks_controller.rb +87 -0
  12. data/app/models/devise_token_auth/concerns/active_record_support.rb +16 -0
  13. data/app/models/devise_token_auth/concerns/mongoid_support.rb +19 -0
  14. data/app/models/devise_token_auth/concerns/tokens_serialization.rb +19 -0
  15. data/app/models/devise_token_auth/concerns/user.rb +253 -0
  16. data/app/models/devise_token_auth/concerns/user_omniauth_callbacks.rb +28 -0
  17. data/app/validators/devise_token_auth_email_validator.rb +23 -0
  18. data/app/views/devise/mailer/confirmation_instructions.html.erb +5 -0
  19. data/app/views/devise/mailer/reset_password_instructions.html.erb +8 -0
  20. data/app/views/devise/mailer/unlock_instructions.html.erb +7 -0
  21. data/app/views/devise_token_auth/omniauth_external_window.html.erb +38 -0
  22. data/config/locales/da-DK.yml +52 -0
  23. data/config/locales/de.yml +51 -0
  24. data/config/locales/en.yml +57 -0
  25. data/config/locales/es.yml +51 -0
  26. data/config/locales/fr.yml +51 -0
  27. data/config/locales/he.yml +52 -0
  28. data/config/locales/it.yml +48 -0
  29. data/config/locales/ja.yml +48 -0
  30. data/config/locales/nl.yml +32 -0
  31. data/config/locales/pl.yml +50 -0
  32. data/config/locales/pt-BR.yml +48 -0
  33. data/config/locales/pt.yml +50 -0
  34. data/config/locales/ro.yml +48 -0
  35. data/config/locales/ru.yml +52 -0
  36. data/config/locales/sq.yml +48 -0
  37. data/config/locales/sv.yml +52 -0
  38. data/config/locales/uk.yml +61 -0
  39. data/config/locales/vi.yml +52 -0
  40. data/config/locales/zh-CN.yml +48 -0
  41. data/config/locales/zh-HK.yml +50 -0
  42. data/config/locales/zh-TW.yml +50 -0
  43. data/lib/devise_tokens.rb +14 -0
  44. data/lib/devise_tokens/blacklist.rb +2 -0
  45. data/lib/devise_tokens/controllers/helpers.rb +161 -0
  46. data/lib/devise_tokens/controllers/url_helpers.rb +10 -0
  47. data/lib/devise_tokens/engine.rb +92 -0
  48. data/lib/devise_tokens/errors.rb +6 -0
  49. data/lib/devise_tokens/rails/routes.rb +116 -0
  50. data/lib/devise_tokens/token_factory.rb +126 -0
  51. data/lib/devise_tokens/url.rb +39 -0
  52. data/lib/devise_tokens/version.rb +3 -0
  53. data/lib/generators/devise_tokens/USAGE +31 -0
  54. data/lib/generators/devise_tokens/install_generator.rb +91 -0
  55. data/lib/generators/devise_tokens/install_generator_helpers.rb +98 -0
  56. data/lib/generators/devise_tokens/install_mongoid_generator.rb +46 -0
  57. data/lib/generators/devise_tokens/install_views_generator.rb +18 -0
  58. data/lib/generators/devise_tokens/templates/devise_tokens.rb +55 -0
  59. data/lib/generators/devise_tokens/templates/devise_tokens_create_users.rb.erb +49 -0
  60. data/lib/generators/devise_tokens/templates/user.rb.erb +9 -0
  61. data/lib/generators/devise_tokens/templates/user_mongoid.rb.erb +56 -0
  62. data/lib/tasks/devise_tokens_tasks.rake +6 -0
  63. metadata +208 -4
  64. 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