devise-tokens 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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