devise-passwordless 0.7.1 → 1.0.0

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.
data/UPGRADING.md ADDED
@@ -0,0 +1,143 @@
1
+ ## Upgrading from 0.x to 1.0
2
+
3
+ ⭐ The 1.0 release includes significant breaking changes! ⭐
4
+
5
+ This is a big release that cleans up the DX quite a bit. 🎉 Make these changes
6
+ to have a successful upgrade:
7
+
8
+ * Generated `MagicLinksController` is no longer required
9
+ * Delete `app/controllers/devise/passwordless/magic_links_controller.rb`
10
+ * Generated `SessionsController` is no longer required
11
+ * If you haven't customized the generated controller in:
12
+
13
+ ```
14
+ app/controllers/devise/passwordless/sessions_controller.rb
15
+ ```
16
+
17
+ then go ahead and delete the file!
18
+ * If you **have** customized the controller, then we're going to move it. Move the file to somewhere like
19
+
20
+ ```
21
+ app/controllers/custom_sessions_controller.rb
22
+ ```
23
+
24
+ And change the inside class definition to match, e.g. from
25
+
26
+ ```ruby
27
+ class Devise::Passwordless::SessionsController < Devise::SessionsController
28
+ ```
29
+
30
+ to
31
+
32
+ ```ruby
33
+ class CustomSessionsController < Devise::Passwordless::SessionsController
34
+ ```
35
+
36
+ Then, change the route of your resource to match it:
37
+
38
+ ```ruby
39
+ devise_for :users,
40
+ controllers: { sessions: "custom_sessions" }
41
+ ```
42
+
43
+ Finally, you should review the latest source of
44
+ `Devise::Passwordless::SessionsController` as its implementation has changed,
45
+ so you'll want to sync up your customizations.
46
+ * Routing no longer requires custom `devise_scope` for magic links
47
+ * Delete any route declarations from `config/routes.rb` that look like this:
48
+
49
+ ```ruby
50
+ devise_scope :user do
51
+ get "/users/magic_link",
52
+ to: "devise/passwordless/magic_links#show",
53
+ as: "users_magic_link"
54
+ ```
55
+
56
+ * Changes are required to the Devise initializer in `config/initializers/devise.rb`:
57
+ * Delete this line:
58
+ ```
59
+ require 'devise/passwordless/mailer'
60
+ ```
61
+ * New config value `passwordless_tokenizer` is required. Check README for
62
+ an explanation of tokenizers.
63
+ * Add this section to `config/initializers/devise.rb`:
64
+
65
+ ```ruby
66
+ # Which algorithm to use for tokenizing magic links. See README for descriptions
67
+ config.passwordless_tokenizer = "MessageEncryptorTokenizer"
68
+ ```
69
+
70
+ * There is a new `:magic_link_sent_paranoid` i18n key you should add to your `config/locales/devise.en.yml` file:
71
+
72
+ ```diff
73
+ @@ -58,6 +58,7 @@
74
+ passwordless:
75
+ not_found_in_database: "Could not find a user for that email address"
76
+ magic_link_sent: "A login link has been sent to your email address. Please follow the link to log in to your account."
77
+ + magic_link_sent_paranoid: "If your account exists, you will receive an email with a login link. Please follow the link to log in to your account."
78
+ errors:
79
+ messages:
80
+ already_confirmed: "was already confirmed, please try signing in"
81
+
82
+ ```
83
+ * If Devise's paranoid mode is enabled in your Devise initializer, this new key
84
+ replaces the use of both `:magic_link_sent` and `:not_found_in_database` to be
85
+ ambiguous about the existence of user accounts to prevent account enumeration
86
+ vulnerabilities. If you want the old behavior back, ensure `Devise.paranoid`
87
+ is `false` by setting `config.paranoid = false` in your Devise initializer.
88
+
89
+ * `magic_link` path and URL helpers now work
90
+ * Delete any code that looks like this:
91
+
92
+ ```ruby
93
+ send("#{@scope_name.to_s.pluralize}_magic_link_url", Hash[@scope_name, {email: @resource.email, token: @token, remember_me: @remember_me}])
94
+ ```
95
+
96
+ and replace it with this:
97
+
98
+ ```ruby
99
+ magic_link_url(@resource, @scope_name => {email: @resource.email, token: @token, remember_me: @remember_me})
100
+ ```
101
+ * The routes are no longer pluralized, so change any references like:
102
+
103
+ ```ruby
104
+ users_magic_link_url
105
+ ```
106
+
107
+ to:
108
+
109
+ ```ruby
110
+ user_magic_link_url
111
+ ```
112
+
113
+ * `Devise::Passwordless::LoginToken` is deprecated.
114
+ * Calls to `::encode` and `::decode` should be replaced with calls to these
115
+ methods on the resource model (e.g. `User`): `#encode_passwordless_token`
116
+ and `::decode_passwordless_token`.
117
+ * References to `Devise::Passwordless::LoginToken.secret_key` should be
118
+ changed to `Devise::Passwordless.secret_key`.
119
+
120
+ * Hotwire/Turbo support
121
+ * If your Rails app uses Hotwire / Turbo, make sure you're using Devise >= 4.9
122
+ and setting the `config.responder` value in your Devise configuration
123
+ (see Devise Turbo upgrade guide: https://github.com/heartcombo/devise/wiki/How-To:-Upgrade-to-Devise-4.9.0-%5BHotwire-Turbo-integration%5D)
124
+
125
+ * The `#send_magic_link` method now uses keyword arguments instead of positional arguments.
126
+ * Change any instances of
127
+
128
+ ```ruby
129
+ remember_me = true
130
+ user.send_magic_link(remember_me, subject: "Custom email subject")
131
+ ```
132
+
133
+ to:
134
+
135
+ ```ruby
136
+ user.send_magic_link(remember_me: true, subject: "Custom email subject")
137
+ ```
138
+
139
+ * After sending a magic link, users will now be redirected rather than
140
+ re-rendering the sign-in form.
141
+ * [See the README][after-magic-link-sent-readme] for details on how to customize the redirect behavior
142
+
143
+ [after-magic-link-sent-readme]: https://github.com/abevoelker/devise-passwordless#redirecting-after-magic-link-is-sent
@@ -1,11 +1,9 @@
1
- # frozen_string_literal: true
2
-
3
- <% module_namespacing do -%>
4
- class Devise::Passwordless::MagicLinksController < DeviseController
1
+ class Devise::MagicLinksController < DeviseController
5
2
  prepend_before_action :require_no_authentication, only: :show
6
3
  prepend_before_action :allow_params_authentication!, only: :show
7
4
  prepend_before_action(only: [:show]) { request.env["devise.skip_timeout"] = true }
8
5
 
6
+ # GET /resource/magic_link
9
7
  def show
10
8
  self.resource = warden.authenticate!(auth_options)
11
9
  set_flash_message!(:notice, :signed_in)
@@ -31,4 +29,3 @@ class Devise::Passwordless::MagicLinksController < DeviseController
31
29
  resource_params.permit(:email, :remember_me)
32
30
  end
33
31
  end
34
- <% end -%>
@@ -0,0 +1,49 @@
1
+ class Devise::Passwordless::SessionsController < Devise::SessionsController
2
+ def create
3
+ if (self.resource = resource_class.find_by(email: create_params[:email]))
4
+ resource.send_magic_link(remember_me: create_params[:remember_me])
5
+ if Devise.paranoid
6
+ set_flash_message!(:notice, :magic_link_sent_paranoid)
7
+ else
8
+ set_flash_message!(:notice, :magic_link_sent)
9
+ end
10
+ else
11
+ self.resource = resource_class.new(create_params)
12
+ if Devise.paranoid
13
+ set_flash_message!(:notice, :magic_link_sent_paranoid)
14
+ else
15
+ set_flash_message!(:alert, :not_found_in_database, now: true)
16
+ render :new, status: devise_error_status
17
+ return
18
+ end
19
+ end
20
+
21
+ redirect_to(after_magic_link_sent_path_for(resource), status: devise_redirect_status)
22
+ end
23
+
24
+ protected
25
+
26
+ def translation_scope
27
+ if action_name == "create"
28
+ "devise.passwordless"
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def create_params
37
+ resource_params.permit(:email, :remember_me)
38
+ end
39
+
40
+ # Devise < 4.9 fallback support
41
+ # See: https://github.com/heartcombo/devise/wiki/How-To:-Upgrade-to-Devise-4.9.0-%5BHotwire-Turbo-integration%5D
42
+ def devise_redirect_status
43
+ Devise.try(:responder).try(:redirect_status) || :found
44
+ end
45
+
46
+ def devise_error_status
47
+ Devise.try(:responder).try(:error_status) || :ok
48
+ end
49
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(ActionMailer)
4
+ module Devise
5
+ module Passwordless
6
+ class Mailer < Devise::Mailer
7
+ def magic_link(record, token, remember_me, opts = {})
8
+ @token = token
9
+ @remember_me = remember_me
10
+ devise_mail(record, :magic_link, opts)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Deny user access when magic link authentication is disabled
4
+ Warden::Manager.after_set_user do |record, warden, options|
5
+ if record && record.respond_to?(:active_for_magic_link_authentication?) && !record.active_for_magic_link_authentication?
6
+ scope = options[:scope]
7
+ warden.logout(scope)
8
+ throw :warden, scope: scope, message: record.magic_link_inactive_message
9
+ end
10
+ end
@@ -1,22 +1,41 @@
1
1
  require 'devise/strategies/magic_link_authenticatable'
2
+ require 'devise/hooks/magic_link_authenticatable'
2
3
 
3
4
  module Devise
4
5
  module Models
5
6
  module MagicLinkAuthenticatable
6
7
  extend ActiveSupport::Concern
7
8
 
8
- def password_required?
9
- false
9
+ # Models using the :database_authenticatable strategy will already
10
+ # have #password_required? and #password defined - we will defer
11
+ # to those methods if they already exist so that people can use
12
+ # both strategies together. Otherwise, for :magic_link_authenticatable-
13
+ # only users, we define them in order to disable password validations:
14
+
15
+ unless instance_methods.include?(:password_required?)
16
+ def password_required?
17
+ false
18
+ end
19
+ end
20
+
21
+ unless instance_methods.include?(:password)
22
+ # Not having a #password method breaks the :validatable module
23
+ #
24
+ # NOTE I proposed a change to Devise to fix this:
25
+ # https://github.com/heartcombo/devise/issues/5346#issuecomment-822022834
26
+ # As of yet it hasn't been accepted due to unknowns of the legacy code's purpose
27
+ def password
28
+ nil
29
+ end
10
30
  end
11
31
 
12
- # Not having a password method breaks the :validatable module
13
- def password
14
- nil
32
+ def encode_passwordless_token(*args, **kwargs)
33
+ self.class.passwordless_tokenizer_class.encode(self, *args, **kwargs)
15
34
  end
16
35
 
17
- def send_magic_link(remember_me, opts = {})
18
- token = Devise::Passwordless::LoginToken.encode(self)
19
- send_devise_notification(:magic_link, token, remember_me, opts)
36
+ def send_magic_link(remember_me: false, **kwargs)
37
+ token = self.encode_passwordless_token
38
+ send_devise_notification(:magic_link, token, remember_me, **kwargs)
20
39
  end
21
40
 
22
41
  # A callback initiated after successfully authenticating. This can be
@@ -32,9 +51,38 @@ module Devise
32
51
  def after_magic_link_authentication
33
52
  end
34
53
 
54
+ # Set this to false to disable magic link auth for this model instance.
55
+ # Magic links will still be generated by the sign-in page, but visiting
56
+ # them will instead display an error message.
57
+ def active_for_magic_link_authentication?
58
+ true
59
+ end
60
+
61
+ # This method determines which error message to display when magic link
62
+ # auth is disabled for this model instance.
63
+ def magic_link_inactive_message
64
+ :magic_link_invalid
65
+ end
66
+
35
67
  protected
36
68
 
37
69
  module ClassMethods
70
+ def passwordless_tokenizer_class
71
+ @passwordless_tokenizer_class ||= self.passwordless_tokenizer.is_a?(Class) ? (
72
+ self.passwordless_tokenizer
73
+ ) : (
74
+ self.passwordless_tokenizer.start_with?("::") ? (
75
+ self.passwordless_tokenizer.constantize
76
+ ) : (
77
+ "Devise::Passwordless::#{self.passwordless_tokenizer}".constantize
78
+ )
79
+ )
80
+ end
81
+
82
+ def decode_passwordless_token(*args, **kwargs)
83
+ passwordless_tokenizer_class.decode(*args, **kwargs)
84
+ end
85
+
38
86
  # We assume this method already gets the sanitized values from the
39
87
  # MagicLinkAuthenticatable strategy. If you are using this method on
40
88
  # your own, be sure to sanitize the conditions hash to only include
@@ -44,8 +92,9 @@ module Devise
44
92
  end
45
93
 
46
94
  Devise::Models.config(self,
95
+ :passwordless_tokenizer,
47
96
  :passwordless_login_within,
48
- :passwordless_secret_key,
97
+ #:passwordless_secret_key,
49
98
  :passwordless_expire_old_tokens_on_sign_in
50
99
  )
51
100
  end
@@ -54,6 +103,27 @@ module Devise
54
103
  end
55
104
 
56
105
  module Devise
106
+ mattr_accessor :passwordless_tokenizer
107
+ @@passwordless_tokenizer = nil
108
+ def self.passwordless_tokenizer
109
+ if @@passwordless_tokenizer.blank?
110
+ Devise::Passwordless.deprecator.warn <<-DEPRECATION.strip_heredoc
111
+ [Devise-Passwordless] `Devise.passwordless_tokenizer` is a required
112
+ config option. If you are upgrading to Devise-Passwordless 1.0 from
113
+ a previous install, you should use "MessageEncryptorTokenizer" for
114
+ backwards compatibility. New installs are templated with
115
+ "SignedGlobalIDTokenizer". Read the README for a comparison of
116
+ options and UPGRADING for upgrade instructions. Execution will
117
+ now proceed with a value of "MessageEncryptorTokenizer" but future
118
+ releases will raise an error if this option is unset.
119
+ DEPRECATION
120
+
121
+ "MessageEncryptorTokenizer"
122
+ else
123
+ @@passwordless_tokenizer
124
+ end
125
+ end
126
+
57
127
  mattr_accessor :passwordless_login_within
58
128
  @@passwordless_login_within = 20.minutes
59
129
 
@@ -0,0 +1,82 @@
1
+ # Monkeypatch to allow multiple routes for a single module
2
+ # TODO this should be submitted as a PR to devise to deleted if/when merged
3
+ module Devise
4
+ class Mapping
5
+ def routes
6
+ @routes ||= ROUTES.values_at(*self.modules).compact.flatten.uniq
7
+ end
8
+ end
9
+ def self.add_module(module_name, options = {})
10
+ options.assert_valid_keys(:strategy, :model, :controller, :route, :no_input, :insert_at)
11
+
12
+ ALL.insert (options[:insert_at] || -1), module_name
13
+
14
+ if strategy = options[:strategy]
15
+ strategy = (strategy == true ? module_name : strategy)
16
+ STRATEGIES[module_name] = strategy
17
+ end
18
+
19
+ if controller = options[:controller]
20
+ controller = (controller == true ? module_name : controller)
21
+ CONTROLLERS[module_name] = controller
22
+ end
23
+
24
+ NO_INPUT << strategy if options[:no_input]
25
+
26
+ if route = options[:route]
27
+ routes = {}
28
+
29
+ case route
30
+ when TrueClass
31
+ routes[module_name] = []
32
+ when Symbol
33
+ routes[route] = []
34
+ when Hash
35
+ routes = route
36
+ else
37
+ raise ArgumentError, ":route should be true, a Symbol or a Hash"
38
+ end
39
+
40
+ routes.each do |key, value|
41
+ URL_HELPERS[key] ||= []
42
+ URL_HELPERS[key].concat(value)
43
+ URL_HELPERS[key].uniq!
44
+
45
+ ROUTES[module_name] = key
46
+ end
47
+
48
+ if routes.size > 1
49
+ ROUTES[module_name] = routes.keys
50
+ end
51
+ end
52
+
53
+ if options[:model]
54
+ path = (options[:model] == true ? "devise/models/#{module_name}" : options[:model])
55
+ camelized = ActiveSupport::Inflector.camelize(module_name.to_s)
56
+ Devise::Models.send(:autoload, camelized.to_sym, path)
57
+ end
58
+
59
+ Devise::Mapping.add_module module_name
60
+ end
61
+ end
62
+
63
+ # Extend Devise's Helpers module to add our after_magic_link_sent_path_for
64
+ # This is defined here as a helper rather than in the sessions controller
65
+ # directly so that it can be overridden in the main ApplicationController
66
+ module Devise
67
+ module Controllers
68
+ module Helpers
69
+ # Method used by sessions controller to redirect user after a magic link
70
+ # is sent from the sign in page. You can overwrite it in your
71
+ # ApplicationController to provide a custom hook for a custom scope.
72
+ #
73
+ # By default it is the root_path.
74
+ def after_magic_link_sent_path_for(resource_or_scope)
75
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
76
+ router_name = Devise.mappings[scope].router_name
77
+ context = router_name ? send(router_name) : self
78
+ context.respond_to?(:root_path) ? context.root_path : "/"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,57 +1,12 @@
1
1
  module Devise::Passwordless
2
2
  class LoginToken
3
- class InvalidOrExpiredTokenError < StandardError; end
4
-
5
- def self.encode(resource)
6
- now = Time.current
7
- len = ActiveSupport::MessageEncryptor.key_len
8
- salt = SecureRandom.random_bytes(len)
9
- key = ActiveSupport::KeyGenerator.new(self.secret_key).generate_key(salt, len)
10
- crypt = ActiveSupport::MessageEncryptor.new(key, serializer: JSON)
11
- encrypted_data = crypt.encrypt_and_sign({
12
- data: {
13
- resource: {
14
- key: resource.to_key,
15
- email: resource.email,
16
- },
17
- },
18
- created_at: now.to_f,
19
- })
20
- salt_base64 = Base64.strict_encode64(salt)
21
- "#{salt_base64}:#{encrypted_data}"
22
- end
23
-
24
- def self.decode(token, as_of=Time.current, expire_duration=Devise.passwordless_login_within)
25
- raise InvalidOrExpiredTokenError if token.blank?
26
- salt_base64, encrypted_data = token.split(":")
27
- begin
28
- salt = Base64.strict_decode64(salt_base64)
29
- rescue ArgumentError
30
- raise InvalidOrExpiredTokenError
31
- end
32
- len = ActiveSupport::MessageEncryptor.key_len
33
- key = ActiveSupport::KeyGenerator.new(self.secret_key).generate_key(salt, len)
34
- crypt = ActiveSupport::MessageEncryptor.new(key, serializer: JSON)
35
- begin
36
- decrypted_data = crypt.decrypt_and_verify(encrypted_data)
37
- rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
38
- raise InvalidOrExpiredTokenError
39
- end
40
-
41
- created_at = ActiveSupport::TimeZone["UTC"].at(decrypted_data["created_at"])
42
- if as_of.to_f > (created_at + expire_duration).to_f
43
- raise InvalidOrExpiredTokenError
44
- end
45
-
46
- decrypted_data
47
- end
48
-
49
3
  def self.secret_key
50
- if Devise.passwordless_secret_key.present?
51
- Devise.passwordless_secret_key
52
- else
53
- Devise.secret_key
54
- end
4
+ Devise::Passwordless.deprecator.warn <<-DEPRECATION.strip_heredoc
5
+ [Devise-Passwordless] `Devise::Passwordless::LoginToken.secret_key` is
6
+ deprecated and will be removed in a future release. Please use
7
+ `Devise::Passwordless.secret_key` instead.
8
+ DEPRECATION
9
+ Devise::Passwordless.secret_key
55
10
  end
56
11
  end
57
- end
12
+ end
@@ -0,0 +1,25 @@
1
+ module Devise::Passwordless
2
+ class Engine < Rails::Engine
3
+ initializer "devise_passwordless.routing" do
4
+ require "devise/passwordless/routing"
5
+
6
+ Devise.add_module(:magic_link_authenticatable, {
7
+ model: true,
8
+ strategy: true,
9
+ route: { magic_link: [nil, :show], session: [nil, :new, :destroy] },
10
+ controller: :sessions,
11
+ })
12
+ end
13
+
14
+ initializer "devise_passwordless.log_filter_check" do
15
+ params = Rails.try(:application).try(:config).try(:filter_parameters) || []
16
+
17
+ unless params.map(&:to_sym).include?(:token)
18
+ warn "[DEVISE-PASSWORDLESS] We have detected that your Rails configuration does not " \
19
+ "filter :token parameters out of your logs. You should append :token to your " \
20
+ "config.filter_parameters Rails setting so that magic link tokens don't " \
21
+ "leak out of your logs."
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ module ActionDispatch::Routing
2
+ class Mapper
3
+
4
+ protected
5
+
6
+ def devise_magic_link(mapping, controllers) #:nodoc:
7
+ resource :magic_link, only: [:show],
8
+ path: mapping.path_names[:magic_link], controller: controllers[:magic_links]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,66 @@
1
+ module Devise::Passwordless
2
+ class MessageEncryptorTokenizer
3
+ def self.encode(resource, extra: nil, expires_at: nil)
4
+ now = Time.current
5
+ len = ActiveSupport::MessageEncryptor.key_len
6
+ salt = SecureRandom.random_bytes(len)
7
+ key = ActiveSupport::KeyGenerator.new(Devise::Passwordless.secret_key).generate_key(salt, len)
8
+ crypt = ActiveSupport::MessageEncryptor.new(key, serializer: JSON)
9
+ data = {
10
+ data: {
11
+ resource: {
12
+ key: resource.to_key,
13
+ email: resource.email,
14
+ },
15
+ },
16
+ created_at: now.to_f,
17
+ }
18
+ data[:data][:extra] = extra if extra
19
+ data[:expires_at] = expires_at.to_f if expires_at
20
+ encrypted_data = crypt.encrypt_and_sign(data)
21
+ salt_base64 = Base64.strict_encode64(salt)
22
+ "#{salt_base64}:#{encrypted_data}"
23
+ end
24
+
25
+ def self.decode(token, resource_class, as_of: Time.current, expire_duration: Devise.passwordless_login_within)
26
+ raise InvalidTokenError if token.blank?
27
+ salt_base64, encrypted_data = token.split(":")
28
+ raise InvalidTokenError if salt_base64.blank? || encrypted_data.blank?
29
+ begin
30
+ salt = Base64.strict_decode64(salt_base64)
31
+ rescue ArgumentError
32
+ raise InvalidTokenError
33
+ end
34
+ len = ActiveSupport::MessageEncryptor.key_len
35
+ key = ActiveSupport::KeyGenerator.new(Devise::Passwordless.secret_key).generate_key(salt, len)
36
+ crypt = ActiveSupport::MessageEncryptor.new(key, serializer: JSON)
37
+ begin
38
+ decrypted_data = crypt.decrypt_and_verify(encrypted_data)
39
+ rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
40
+ raise InvalidTokenError
41
+ end
42
+
43
+ unless (expiration_time = decrypted_data["expires_at"])
44
+ created_at = ActiveSupport::TimeZone["UTC"].at(decrypted_data["created_at"])
45
+ expiration_time = (created_at + expire_duration).to_f
46
+ end
47
+
48
+ if as_of.to_f > expiration_time
49
+ raise ExpiredTokenError
50
+ end
51
+
52
+ resource = resource_class.find_by(id: decrypted_data["data"]["resource"]["key"])
53
+
54
+ if resource_class.passwordless_expire_old_tokens_on_sign_in
55
+ if (last_login = resource.try(:current_sign_in_at))
56
+ token_created_at = ActiveSupport::TimeZone["UTC"].at(decrypted_data["created_at"])
57
+ if token_created_at < last_login
58
+ raise ExpiredTokenError
59
+ end
60
+ end
61
+ end
62
+
63
+ [resource, decrypted_data]
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,20 @@
1
+ require "globalid"
2
+
3
+ module Devise::Passwordless
4
+ class SignedGlobalIDTokenizer
5
+ def self.encode(resource, expires_in: nil, expires_at: nil)
6
+ if expires_at
7
+ resource.to_sgid(expires_at: expires_at, for: "login").to_s
8
+ else
9
+ resource.to_sgid(expires_in: expires_in || resource.class.passwordless_login_within, for: "login").to_s
10
+ end
11
+ end
12
+
13
+ def self.decode(token, resource_class)
14
+ resource = GlobalID::Locator.locate_signed(token, for: "login")
15
+ raise ExpiredTokenError unless resource
16
+ raise InvalidTokenError if resource.class != resource_class
17
+ [resource, {}]
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Devise
2
4
  module Passwordless
3
- VERSION = "0.7.1"
5
+ VERSION = "1.0.0"
4
6
  end
5
7
  end
@@ -1,3 +1,27 @@
1
1
  require "devise/passwordless/version"
2
+ require "devise/monkeypatch"
3
+ require "devise/passwordless/rails" if defined?(Rails::Engine)
2
4
  require "devise/models/magic_link_authenticatable"
3
5
  require "generators/devise/passwordless/install_generator"
6
+ require "devise/passwordless/tokenizers/message_encryptor_tokenizer"
7
+ require "devise/passwordless/tokenizers/signed_global_id_tokenizer"
8
+
9
+ module Devise
10
+ module Passwordless
11
+ class InvalidOrExpiredTokenError < StandardError; end
12
+ class InvalidTokenError < InvalidOrExpiredTokenError; end
13
+ class ExpiredTokenError < InvalidOrExpiredTokenError; end
14
+
15
+ def self.deprecator
16
+ @deprecator ||= ActiveSupport::Deprecation.new("1.1", "Devise-Passwordless")
17
+ end
18
+
19
+ def self.secret_key
20
+ if Devise.passwordless_secret_key.present?
21
+ Devise.passwordless_secret_key
22
+ else
23
+ Devise.secret_key
24
+ end
25
+ end
26
+ end
27
+ end