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 +4 -4
- data/README.md +93 -20
- data/lib/devise/models/{email_authenticatable.rb → magic_link_authenticatable.rb} +11 -6
- data/lib/devise/passwordless.rb +1 -1
- data/lib/devise/passwordless/version.rb +1 -1
- data/lib/devise/strategies/{email_authenticatable.rb → magic_link_authenticatable.rb} +6 -7
- data/lib/generators/devise/passwordless/controller_generator.rb +21 -0
- data/lib/generators/devise/passwordless/install_generator.rb +58 -15
- data/lib/generators/devise/passwordless/templates/magic_links_controller.rb.erb +33 -0
- data/lib/generators/devise/passwordless/templates/sessions_controller.rb.erb +34 -0
- metadata +7 -5
- data/lib/devise/passwordless/mailer.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9bb24b5fe4c2794fc255797ca018533757e999c80ef5ac8f2992e245b48bec1
|
4
|
+
data.tar.gz: c38c95a39eaae489078f4730f96c4dd8ebda460028cd2ef752196396e2b35837
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
17
|
+
And then execute:
|
14
18
|
|
15
19
|
```
|
16
|
-
|
20
|
+
$ bundle install
|
17
21
|
```
|
18
22
|
|
19
|
-
|
23
|
+
Finally, run the install generator:
|
20
24
|
|
21
|
-
```
|
22
|
-
|
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 `:
|
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
|
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 :
|
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
|
-
|
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
|
-
|
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/
|
1
|
+
require 'devise/strategies/magic_link_authenticatable'
|
2
2
|
|
3
3
|
module Devise
|
4
4
|
module Models
|
5
|
-
module
|
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
|
28
|
+
# def after_magic_link_authentication
|
24
29
|
# self.update_attribute(:invite_code, nil)
|
25
30
|
# end
|
26
31
|
#
|
27
|
-
def
|
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
|
-
#
|
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
|
42
|
+
def find_for_magic_link_authentication(conditions)
|
38
43
|
find_for_authentication(conditions)
|
39
44
|
end
|
40
45
|
|
data/lib/devise/passwordless.rb
CHANGED
@@ -6,7 +6,7 @@ require "devise/passwordless/login_token"
|
|
6
6
|
|
7
7
|
module Devise
|
8
8
|
module Strategies
|
9
|
-
class
|
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.
|
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(:
|
55
|
+
Warden::Strategies.add(:magic_link_authenticatable, Devise::Strategies::MagicLinkAuthenticatable)
|
56
56
|
|
57
|
-
Devise.add_module(:
|
57
|
+
Devise.add_module(:magic_link_authenticatable, {
|
58
58
|
strategy: true,
|
59
59
|
controller: :sessions,
|
60
|
-
|
61
|
-
|
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 "
|
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 :
|
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
|
-
#
|
19
|
-
#
|
20
|
-
#
|
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
|
60
|
+
STDERR.puts "Couldn't find #{devise_yaml} - skipping patch"
|
32
61
|
return
|
33
62
|
end
|
34
|
-
config
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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.
|
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-
|
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/
|
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/
|
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
|