devise-passwordless 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '08511edac883e2b94811e349d41ce761a7d5771e55b628450bd1e02d835f0374'
4
- data.tar.gz: 9d0f442ffed9f59818a370c8bdc815f362367bd24a6c09fc66b0097009992cd9
3
+ metadata.gz: a9bb24b5fe4c2794fc255797ca018533757e999c80ef5ac8f2992e245b48bec1
4
+ data.tar.gz: c38c95a39eaae489078f4730f96c4dd8ebda460028cd2ef752196396e2b35837
5
5
  SHA512:
6
- metadata.gz: 405e6a4ee5dbb3c66dcd079a92642e01dfb10b336d5e220f8a05f0814a2ac50ef47e78aa13e734b4aec4ce2891eb8cde900854984c3c84193afb1ff05e5b8481
7
- data.tar.gz: 933e273120b795b3b1eb1bb96a4672c9f3da7645ed7df55046eddc57197307afd9aadee5612fb047980cceed3fc5c2348f5388bb5b19df87191e9ee57e2fdb0f
6
+ metadata.gz: 821b144072819bf40fdc6687184262af929cfc066173700b7afdb92a56a344e6c01c923bd956a3ae928c8dab572f8ff2f84de73c56fde29139e2c28f8564fe0c
7
+ data.tar.gz: 51bc2079671c704e365e43b7ad10d72359b8ff2588c31deee1ab001ab2f93f4d6fe3992a1de7ea890401dbf1be08ae912e4302fd7c673b6b61bc3ba29832faae
data/README.md CHANGED
@@ -1,42 +1,43 @@
1
1
  # Devise::Passwordless
2
2
 
3
- A passwordless login strategy for [Devise][]
3
+ A passwordless a.k.a. "magic link" login strategy for [Devise][]
4
+
5
+ No database migrations are needed as login links are stateless, encrypted tokens generated with Rails's MessageEncryptor.
4
6
 
5
7
  ## Installation
6
8
 
7
- You should already have Devise installed. Then add this gem:
9
+ First, install and set up [Devise][].
10
+
11
+ Then add this gem to your application's Gemfile:
8
12
 
9
13
  ```ruby
10
14
  gem "devise-passwordless"
11
15
  ```
12
16
 
13
- Then run the generator to automatically update your Devise initializer:
17
+ And then execute:
14
18
 
15
19
  ```
16
- rails g devise:passwordless:install
20
+ $ bundle install
17
21
  ```
18
22
 
19
- Merge these YAML values into your `devise.en.yml` file:
23
+ Finally, run the install generator:
20
24
 
21
- ```yaml
22
- en:
23
- devise:
24
- failure:
25
- passwordless_invalid: "Invalid or expired login link."
26
- mailer:
27
- passwordless_link:
28
- subject: "Here's your magic link"
25
+ ```
26
+ $ rails g devise:passwordless:install
29
27
  ```
30
28
 
29
+ See the [customization section](#customization) for details on what gets installed and how to configure and customize.
30
+
31
31
  ## Usage
32
32
 
33
- This gem adds an `:email_authenticatable` strategy that can be used in your Devise models for passwordless authentication. This strategy plays well with most other Devise strategies.
33
+ This gem adds an `:magic_link_authenticatable` strategy that can be used in your Devise models for passwordless authentication. This strategy plays well with most other Devise strategies (see [*notes on other Devise strategies*](#notes-on-other-devise-strategies)).
34
34
 
35
- For example, for a User model, you could do this (other strategies optional and not an exhaustive list):
35
+ For example, for a User model, you could do this (other strategies listed are optional and not exhaustive):
36
36
 
37
37
  ```ruby
38
+ # app/models/user.rb
38
39
  class User < ApplicationRecord
39
- devise :email_authenticatable,
40
+ devise :magic_link_authenticatable,
40
41
  :registerable,
41
42
  :rememberable,
42
43
  :validatable,
@@ -44,7 +45,81 @@ class User < ApplicationRecord
44
45
  end
45
46
  ```
46
47
 
47
- **Note** if using the `:rememberable` strategy for "remember me" functionality, you'll need to add a `remember_token` column to your resource, as there is no password salt to use for validating cookies:
48
+ Then, you'll need to generate two controllers to modify Devise's default session create logic and to handle processing magic links:
49
+
50
+ ```
51
+ $ rails g devise:passwordless:controller User
52
+ ```
53
+
54
+ Then, modify your routes file like so to use these controllers:
55
+
56
+ ```ruby
57
+ # config/routes.rb
58
+ Rails.application.routes.draw do
59
+ devise_for :users, controllers: { sessions: "users/sessions" }
60
+ devise_scope :user do
61
+ get "/users/magic_links" => "users/magic_links#show"
62
+ end
63
+ end
64
+ ```
65
+
66
+ Finally, you'll want to update Devise's generated views to remove references to passwords.
67
+
68
+ These files/directories can be deleted entirely:
69
+
70
+ * `app/views/devise/passwords`
71
+ * `app/views/devise/mailer/password_change.html.erb`
72
+ * `app/views/devise/mailer/reset_password_instructions.html.erb`
73
+
74
+ And these should be edited to remove password references:
75
+
76
+ * `app/views/devise/registrations/new.html.erb`
77
+ * Delete fields `:password` and `:password_confirmation`
78
+ * `app/views/devise/registrations/edit.html.erb`
79
+ * Delete fields `:password`, `:password_confirmation`, `:current_password`
80
+ * `app/views/devise/sessions/new.html.erb`
81
+ * Delete field `:password`
82
+
83
+ ## Customization
84
+
85
+ Configuration options are stored in Devise's initializer at `config/initializers/devise.rb`:
86
+
87
+ ```ruby
88
+ # ==> Configuration for :magic_link_authenticatable
89
+
90
+ # Need to use a custom Devise mailer in order to send magic links
91
+ config.mailer = "PasswordlessMailer"
92
+
93
+ # Time period after a magic login link is sent out that it will be valid for.
94
+ # config.passwordless_login_within = 20.minutes
95
+
96
+ # The secret key used to generate passwordless login tokens. The default value
97
+ # is nil, which means defer to Devise's `secret_key` config value. Changing this
98
+ # key will render invalid all existing passwordless login tokens. You can
99
+ # generate your own secret value with e.g. `rake secret`
100
+ # config.passwordless_secret_key = nil
101
+ ```
102
+
103
+ To customize the magic link email subject line and other status and error messages, modify these values in `config/locales/devise.en.yml`:
104
+
105
+ ```yaml
106
+ en:
107
+ devise:
108
+ passwordless:
109
+ not_found_in_database: "Could not find a user for that email address"
110
+ magic_link_sent: "A login link has been sent to your email address. Please follow the link to log in to your account."
111
+ failure:
112
+ passwordless_invalid: "Invalid or expired login link."
113
+ mailer:
114
+ magic_link:
115
+ subject: "Here's your magic login link 🪄✨"
116
+ ```
117
+
118
+ To customize the magic link email body, edit `app/views/devise/mailer/magic_link.html.erb`
119
+
120
+ ### Notes on other Devise strategies
121
+
122
+ If using the `:rememberable` strategy for "remember me" functionality, you'll need to add a `remember_token` column to your resource, as by default it assumes you're using a password strategy and relies on comparing the password's salt to validate cookies:
48
123
 
49
124
  ```ruby
50
125
  change_table :users do |t|
@@ -52,9 +127,7 @@ change_table :users do |t|
52
127
  end
53
128
  ```
54
129
 
55
- **Note** if using the `:confirmable` strategy, you may want to override the default Devise behavior of requiring a fresh login after email confirmation (e.g. [this](https://stackoverflow.com/a/39010334/215168) or [this](https://stackoverflow.com/a/25865526/215168) approach). Otherwise, users will have to get a fresh login link after confirming their email, which makes no sense if they just confirmed they own the email address.
56
-
57
- ## Configuration
130
+ If using the `:confirmable` strategy, you may want to override the default Devise behavior of requiring a fresh login after email confirmation (e.g. [this](https://stackoverflow.com/a/39010334/215168) or [this](https://stackoverflow.com/a/25865526/215168) approach). Otherwise, users will have to get a fresh login link after confirming their email, which makes little sense if they just confirmed they own the email address.
58
131
 
59
132
  ## License
60
133
 
@@ -1,8 +1,8 @@
1
- require 'devise/strategies/email_authenticatable'
1
+ require 'devise/strategies/magic_link_authenticatable'
2
2
 
3
3
  module Devise
4
4
  module Models
5
- module EmailAuthenticatable
5
+ module MagicLinkAuthenticatable
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  def password_required?
@@ -14,27 +14,32 @@ module Devise
14
14
  nil
15
15
  end
16
16
 
17
+ def send_magic_link(remember_me)
18
+ token = Devise::Passwordless::LoginToken.encode(self)
19
+ send_devise_notification(:magic_link, token, remember_me, {})
20
+ end
21
+
17
22
  # A callback initiated after successfully authenticating. This can be
18
23
  # used to insert your own logic that is only run after the user successfully
19
24
  # authenticates.
20
25
  #
21
26
  # Example:
22
27
  #
23
- # def after_passwordless_authentication
28
+ # def after_magic_link_authentication
24
29
  # self.update_attribute(:invite_code, nil)
25
30
  # end
26
31
  #
27
- def after_passwordless_authentication
32
+ def after_magic_link_authentication
28
33
  end
29
34
 
30
35
  protected
31
36
 
32
37
  module ClassMethods
33
38
  # We assume this method already gets the sanitized values from the
34
- # EmailAuthenticatable strategy. If you are using this method on
39
+ # MagicLinkAuthenticatable strategy. If you are using this method on
35
40
  # your own, be sure to sanitize the conditions hash to only include
36
41
  # the proper fields.
37
- def find_for_email_authentication(conditions)
42
+ def find_for_magic_link_authentication(conditions)
38
43
  find_for_authentication(conditions)
39
44
  end
40
45
 
@@ -1,3 +1,3 @@
1
1
  require "devise/passwordless/version"
2
- require "devise/models/email_authenticatable"
2
+ require "devise/models/magic_link_authenticatable"
3
3
  require "generators/devise/passwordless/install_generator"
@@ -1,5 +1,5 @@
1
1
  module Devise
2
2
  module Passwordless
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -6,7 +6,7 @@ require "devise/passwordless/login_token"
6
6
 
7
7
  module Devise
8
8
  module Strategies
9
- class EmailAuthenticatable < Authenticatable
9
+ class MagicLinkAuthenticatable < Authenticatable
10
10
  #undef :password
11
11
  #undef :password=
12
12
  attr_accessor :token
@@ -31,7 +31,7 @@ module Devise
31
31
  resource = mapping.to.find_by(id: data["resource"]["key"])
32
32
  if validate(resource)
33
33
  remember_me(resource)
34
- resource.after_passwordless_authentication
34
+ resource.after_magic_link_authentication
35
35
  success!(resource)
36
36
  else
37
37
  fail!(:passwordless_invalid)
@@ -52,12 +52,11 @@ module Devise
52
52
  end
53
53
  end
54
54
 
55
- Warden::Strategies.add(:email_authenticatable, Devise::Strategies::EmailAuthenticatable)
55
+ Warden::Strategies.add(:magic_link_authenticatable, Devise::Strategies::MagicLinkAuthenticatable)
56
56
 
57
- Devise.add_module(:email_authenticatable, {
57
+ Devise.add_module(:magic_link_authenticatable, {
58
58
  strategy: true,
59
59
  controller: :sessions,
60
- model: "devise/models/email_authenticatable",
61
- #route: { email_authenticatable: [nil, :new, :edit] }
62
- route: :session
60
+ route: :session,
61
+ model: "devise/models/magic_link_authenticatable",
63
62
  })
@@ -0,0 +1,21 @@
1
+ require "rails/generators/named_base"
2
+
3
+ module Devise::Passwordless
4
+ module Generators # :nodoc:
5
+ class ControllerGenerator < ::Rails::Generators::NamedBase # :nodoc:
6
+ desc "Creates the session and magic link controllers needed for a Devise resource to use passwordless auth"
7
+
8
+ def self.default_generator_root
9
+ File.dirname(__FILE__)
10
+ end
11
+
12
+ def create_sessions_controller
13
+ template "sessions_controller.rb.erb", File.join("app/controllers", class_path, plural_name, "sessions_controller.rb")
14
+ end
15
+
16
+ def create_magic_links_controller
17
+ template "magic_links_controller.rb.erb", File.join("app/controllers", class_path, plural_name, "magic_links_controller.rb")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,40 +2,83 @@ require "rails/generators"
2
2
  require "yaml"
3
3
 
4
4
  module Devise::Passwordless
5
- module Generators
6
- class InstallGenerator < ::Rails::Generators::Base
7
- desc "Updates the Devise initializer to add passwordless config options"
5
+ module Generators # :nodoc:
6
+ class InstallGenerator < ::Rails::Generators::Base # :nodoc:
7
+ desc "Creates default install and config files for the Devise passwordless auth strategy"
8
8
 
9
9
  def update_devise_initializer
10
10
  inject_into_file 'config/initializers/devise.rb', before: /^end$/ do <<~'CONFIG'.indent(2)
11
11
 
12
- # ==> Configuration for :email_authenticatable
12
+ # ==> Configuration for :magic_link_authenticatable
13
+
14
+ # Need to use a custom Devise mailer in order to send magic links
15
+ config.mailer = "PasswordlessMailer"
13
16
 
14
17
  # Time period after a magic login link is sent out that it will be valid for.
15
18
  # config.passwordless_login_within = 20.minutes
16
-
17
- # The secret key used to generate passwordless login tokens. The default
18
- # value is nil, which means defer to Devise's `secret_key` config value.
19
- # Changing this key will render invalid all existing passwordless login
20
- # tokens. You can generate your own value with e.g. `rake secret`
19
+
20
+ # The secret key used to generate passwordless login tokens. The default value
21
+ # is nil, which means defer to Devise's `secret_key` config value. Changing this
22
+ # key will render invalid all existing passwordless login tokens. You can
23
+ # generate your own secret value with e.g. `rake secret`
21
24
  # config.passwordless_secret_key = nil
22
25
  CONFIG
23
26
  end
24
27
  end
25
28
 
29
+ def add_custom_devise_mailer
30
+ create_file "app/mailers/passwordless_mailer.rb" do <<~'FILE'
31
+ class PasswordlessMailer < Devise::Mailer
32
+ def magic_link(record, token, remember_me, opts = {})
33
+ @token = token
34
+ @remember_me = remember_me
35
+ devise_mail(record, :magic_link, opts)
36
+ end
37
+ end
38
+ FILE
39
+ end
40
+ end
41
+
42
+ def add_mailer_view
43
+ create_file "app/views/devise/mailer/magic_link.html.erb" do <<~'FILE'
44
+ <p>Hello <%= @resource.email %>!</p>
45
+
46
+ <p>You can login using the link below:</p>
47
+
48
+ <p><%= link_to "Log in to my account", send("#{@scope_name.to_s.pluralize}_magic_links_url", Hash[@scope_name, {email: @resource.email, token: @token, remember_me: @remember_me}]) %></p>
49
+
50
+ <p>Note that the link will expire in <%= Devise.passwordless_login_within.inspect %>.</p>
51
+ FILE
52
+ end
53
+ end
54
+
26
55
  def update_devise_yaml
27
56
  devise_yaml = "config/locales/devise.en.yml"
28
57
  begin
29
58
  config = YAML.load_file(devise_yaml)
30
59
  rescue Errno::ENOENT
31
- STDERR.puts "Couldn't find devise.en.yml - skipping patch"
60
+ STDERR.puts "Couldn't find #{devise_yaml} - skipping patch"
32
61
  return
33
62
  end
34
- config["en"]["devise"]["failure"]["passwordless_invalid"] = "Invalid or expired login link."
35
- config["en"]["devise"]["mailer"]["passwordless_link"] = {subject: "Your login link"}
36
- File.open(devise_yaml, "w") do |f|
37
- f.write(config.to_yaml)
38
- end
63
+ config.deep_merge!({
64
+ en: {
65
+ devise: {
66
+ passwordless: {
67
+ not_found_in_database: "Could not find a user for that email address",
68
+ magic_link_sent: "A login link has been sent to your email address. Please follow the link to log in to your account.",
69
+ },
70
+ failure: {
71
+ passwordless_invalid: "Invalid or expired login link.",
72
+ },
73
+ mailer: {
74
+ magic_link: {
75
+ subject: "Here's your login link 🪄✨",
76
+ },
77
+ }
78
+ }
79
+ }
80
+ })
81
+ File.open(devise_yaml, "w"){ |f| f.write(config.to_yaml) }
39
82
  end
40
83
  end
41
84
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= class_name.pluralize %>::MagicLinksController < DeviseController
5
+ prepend_before_action :require_no_authentication, only: :show
6
+ prepend_before_action :allow_params_authentication!, only: :show
7
+ prepend_before_action(only: [:show]) { request.env["devise.skip_timeout"] = true }
8
+
9
+ def show
10
+ self.resource = warden.authenticate!(auth_options)
11
+ set_flash_message!(:notice, :signed_in)
12
+ sign_in(resource_name, resource)
13
+ yield resource if block_given?
14
+ redirect_to after_sign_in_path_for(resource)
15
+ end
16
+
17
+ protected
18
+
19
+ def auth_options
20
+ { scope: resource_name, recall: "#{resource_name.to_s.pluralize}/sessions#new" }
21
+ end
22
+
23
+ def translation_scope
24
+ "devise.sessions"
25
+ end
26
+
27
+ private
28
+
29
+ def create_params
30
+ resource_params.permit(:email, :remember_me)
31
+ end
32
+ end
33
+ <% end -%>
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= class_name.pluralize %>::SessionsController < Devise::SessionsController
5
+ def create
6
+ self.resource = resource_class.find_by(email: create_params[:email])
7
+ if self.resource
8
+ resource.send_magic_link(create_params[:remember_me])
9
+ set_flash_message(:notice, :magic_link_sent, now: true)
10
+ else
11
+ set_flash_message(:alert, :not_found_in_database, now: true)
12
+ end
13
+
14
+ self.resource = resource_class.new(create_params)
15
+ render :new
16
+ end
17
+
18
+ protected
19
+
20
+ def translation_scope
21
+ if action_name == "create"
22
+ "devise.passwordless"
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def create_params
31
+ resource_params.permit(:email, :remember_me)
32
+ end
33
+ end
34
+ <% end -%>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise-passwordless
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abe Voelker
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-09 00:00:00.000000000 Z
11
+ date: 2020-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: devise
@@ -97,13 +97,15 @@ files:
97
97
  - bin/console
98
98
  - bin/setup
99
99
  - devise-passwordless.gemspec
100
- - lib/devise/models/email_authenticatable.rb
100
+ - lib/devise/models/magic_link_authenticatable.rb
101
101
  - lib/devise/passwordless.rb
102
102
  - lib/devise/passwordless/login_token.rb
103
- - lib/devise/passwordless/mailer.rb
104
103
  - lib/devise/passwordless/version.rb
105
- - lib/devise/strategies/email_authenticatable.rb
104
+ - lib/devise/strategies/magic_link_authenticatable.rb
105
+ - lib/generators/devise/passwordless/controller_generator.rb
106
106
  - lib/generators/devise/passwordless/install_generator.rb
107
+ - lib/generators/devise/passwordless/templates/magic_links_controller.rb.erb
108
+ - lib/generators/devise/passwordless/templates/sessions_controller.rb.erb
107
109
  homepage: https://github.com/abevoelker/devise-passwordless
108
110
  licenses:
109
111
  - MIT
@@ -1,9 +0,0 @@
1
- if defined?(Devise::Mailer)
2
- Devise::Mailer.class_eval do
3
- def passwordless_link(record, remember_me, opts = {})
4
- @remember_me = remember_me
5
- @token = Devise::Passwordless::LoginToken.encode(record)
6
- devise_mail(record, :passwordless_link, opts)
7
- end
8
- end
9
- end