devise-passwordless 0.1.0 → 0.2.0

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