devise-passwordless 0.7.1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,20 @@
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
+ ::Devise::Passwordless.check_filter_parameters(params)
18
+ end
19
+ end
20
+ 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.1"
4
6
  end
5
7
  end