rails_authentication 0.2.0 → 0.3.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 +17 -3
- data/lib/generators/authentication/authentication_generator.rb +15 -0
- data/lib/generators/authentication/features/magic_link.rb +28 -0
- data/lib/generators/authentication/templates/app/controllers/magic_links_controller.rb.tt +67 -0
- data/lib/generators/authentication/templates/app/mailers/magic_links_mailer.rb.tt +6 -0
- data/lib/generators/authentication/templates/app/models/concerns/magic_link_concern.rb.tt +34 -0
- data/lib/generators/authentication/templates/app/views/magic_links/new.html.erb.tt +12 -0
- data/lib/generators/authentication/templates/app/views/magic_links_mailer/magic_link.html.erb.tt +6 -0
- data/lib/generators/authentication/templates/app/views/magic_links_mailer/magic_link.text.erb.tt +5 -0
- data/lib/generators/authentication/templates/app/views/sessions/new.html.erb.tt +4 -1
- data/lib/generators/authentication/templates/db/migrate/add_magic_link_to_users.rb.tt +8 -0
- data/lib/rails_authentication/version.rb +1 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c71c1a30969abed6c581c67e3447c1968176e6e25c218894e64d5317472b873e
|
|
4
|
+
data.tar.gz: 1f65cac490fae4c9f575d907bf70d4d059afdb1bcb5e8a56474b741b4eac4f3e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5469da8db1ff529c5dd079b37a3499b1bc08b657560bdcd037607c407d6be292a306bcd146bcc99f30243435824a6fa9a58cb965a80edc0d7fb30435dcdc1cc3
|
|
7
|
+
data.tar.gz: '0693170a1f4bdebe7ec41bfe6088ddbbc436225aa3a09b8d3216af9cf800bd36d461a9618fd022e0b45b1662c82382b13a0bf4cc10e715a9e7247fcd1d77fb18'
|
data/README.md
CHANGED
|
@@ -17,6 +17,7 @@ gem extends that same command to also install:
|
|
|
17
17
|
| **Validatable** | Email format/uniqueness, password length, and password complexity validations |
|
|
18
18
|
| **Lockable** | Locks the account after 5 failed attempts; unlock via email or automatically after 1 hour |
|
|
19
19
|
| **Invitable** | Invite users by email; blocks sign-in until they accept by choosing their own password |
|
|
20
|
+
| **MagicLink** (opt-in) | Passwordless magic link sign-in via email — DB-backed, single-use, expiring tokens |
|
|
20
21
|
|
|
21
22
|
Everything is **generated into your app** as plain, readable code — controllers, views, mailers,
|
|
22
23
|
migrations, and one model concern per feature. There is no runtime dependency on this gem: after
|
|
@@ -27,7 +28,7 @@ This gem is meant to be installed temporarily. Install it long enough to run the
|
|
|
27
28
|
## Installation
|
|
28
29
|
|
|
29
30
|
Requires Rails >= 8.0 and, for the email-driven features (Confirmable, Recoverable, Lockable,
|
|
30
|
-
Invitable), Action Mailer.
|
|
31
|
+
Invitable, MagicLink), Action Mailer.
|
|
31
32
|
|
|
32
33
|
```ruby
|
|
33
34
|
# Gemfile
|
|
@@ -55,6 +56,18 @@ Available flags: `--skip-confirmable`, `--skip-recoverable`, `--skip-registerabl
|
|
|
55
56
|
`--skip-lockable`, `--skip-invitable`, and `--reconfirmable` (Confirmable: postpone email address
|
|
56
57
|
changes until the new address is confirmed, via an `unconfirmed_email` column).
|
|
57
58
|
|
|
59
|
+
MagicLink is the one **opt-in** feature — it changes the sign-in UX, so you have to ask for it:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
bin/rails generate authentication --magic-link
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
It adds a "Sign in with magic link" link to the sign-in page, leading to an email-only form. The
|
|
66
|
+
emailed link signs the user in directly; tokens are DB-backed, single-use, and expire after
|
|
67
|
+
20 minutes (`MagicLinkConcern::MAGIC_LINK_EXPIRES_IN`). The magic link flow honors the other
|
|
68
|
+
enabled features: locked, unconfirmed, or invitation-pending accounts still can't sign in, and
|
|
69
|
+
Trackable records the attempt.
|
|
70
|
+
|
|
58
71
|
Each feature adds a single `include <Feature>Concern` line to `app/models/user.rb`; all of its
|
|
59
72
|
model behavior lives in `app/models/concerns/<feature>_concern.rb`. Tunables are plain constants in
|
|
60
73
|
the generated concerns — e.g. `TimeoutableConcern::TIMEOUT_IN`, `LockableConcern::MAXIMUM_ATTEMPTS`,
|
|
@@ -69,6 +82,7 @@ resource :registration, only: %i[ new create edit update destroy ]
|
|
|
69
82
|
resources :confirmations, only: %i[ new create show ], param: :token
|
|
70
83
|
resources :unlocks, only: %i[ new create show ], param: :token
|
|
71
84
|
resources :invitations, only: %i[ new create edit update ], param: :token
|
|
85
|
+
resources :magic_links, only: %i[ new create show ], param: :token # with --magic-link
|
|
72
86
|
resource :session # from the base generator
|
|
73
87
|
resources :passwords, param: :token # from the base generator
|
|
74
88
|
```
|
|
@@ -87,8 +101,8 @@ resources :passwords, param: :token # from the base generator
|
|
|
87
101
|
|
|
88
102
|
## Future Plans
|
|
89
103
|
|
|
90
|
-
-
|
|
91
|
-
-
|
|
104
|
+
- OTT (Email code)
|
|
105
|
+
- Passkey
|
|
92
106
|
|
|
93
107
|
## Development
|
|
94
108
|
|
|
@@ -11,6 +11,7 @@ require_relative "features/timeoutable"
|
|
|
11
11
|
require_relative "features/validatable"
|
|
12
12
|
require_relative "features/lockable"
|
|
13
13
|
require_relative "features/invitable"
|
|
14
|
+
require_relative "features/magic_link"
|
|
14
15
|
|
|
15
16
|
module RailsAuthentication
|
|
16
17
|
module Generators
|
|
@@ -42,6 +43,7 @@ module RailsAuthentication
|
|
|
42
43
|
include Features::Validatable
|
|
43
44
|
include Features::Lockable
|
|
44
45
|
include Features::Invitable
|
|
46
|
+
include Features::MagicLink
|
|
45
47
|
|
|
46
48
|
source_root File.expand_path("templates", __dir__)
|
|
47
49
|
|
|
@@ -53,6 +55,11 @@ module RailsAuthentication
|
|
|
53
55
|
class_option :reconfirmable, type: :boolean, default: false,
|
|
54
56
|
desc: "Confirmable: postpone email address changes until reconfirmed (adds unconfirmed_email column)"
|
|
55
57
|
|
|
58
|
+
# Unlike the FEATURES above (default-on, opt-out via --skip-<feature>),
|
|
59
|
+
# magic link is opt-in: it changes the sign-in UX, so it must be requested.
|
|
60
|
+
class_option :magic_link, type: :boolean, default: false,
|
|
61
|
+
desc: "Add passwordless magic link sign-in (opt-in)"
|
|
62
|
+
|
|
56
63
|
def install_base_authentication
|
|
57
64
|
say "Running Rails' built-in authentication generator", :green
|
|
58
65
|
Rails::Generators.invoke("rails:authentication", [], behavior: behavior, destination_root: destination_root)
|
|
@@ -94,6 +101,10 @@ module RailsAuthentication
|
|
|
94
101
|
generate_invitable if invitable?
|
|
95
102
|
end
|
|
96
103
|
|
|
104
|
+
def install_magic_link
|
|
105
|
+
generate_magic_link if magic_link?
|
|
106
|
+
end
|
|
107
|
+
|
|
97
108
|
# Runs after every feature install so the blank line separates the concern
|
|
98
109
|
# includes (if any) from the rest of the class body, no matter which features
|
|
99
110
|
# are enabled.
|
|
@@ -120,6 +131,10 @@ module RailsAuthentication
|
|
|
120
131
|
confirmable? && options[:reconfirmable]
|
|
121
132
|
end
|
|
122
133
|
|
|
134
|
+
def magic_link?
|
|
135
|
+
options[:magic_link]
|
|
136
|
+
end
|
|
137
|
+
|
|
123
138
|
def include_concern_in_user(concern)
|
|
124
139
|
inject_into_class "app/models/user.rb", "User", " include #{concern}\n"
|
|
125
140
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAuthentication
|
|
4
|
+
module Generators
|
|
5
|
+
module Features
|
|
6
|
+
# Opt-in (--magic-link): magic link sign-in alongside password sign-in.
|
|
7
|
+
# Tokens are DB-backed and single-use, consistent with this gem's
|
|
8
|
+
# Recoverable rework.
|
|
9
|
+
module MagicLink
|
|
10
|
+
private
|
|
11
|
+
def generate_magic_link
|
|
12
|
+
template "app/models/concerns/magic_link_concern.rb"
|
|
13
|
+
include_concern_in_user "MagicLinkConcern"
|
|
14
|
+
migration_template "db/migrate/add_magic_link_to_users.rb", "db/migrate/add_magic_link_to_users.rb"
|
|
15
|
+
template "app/controllers/magic_links_controller.rb"
|
|
16
|
+
template "app/views/magic_links/new.html.erb"
|
|
17
|
+
route "resources :magic_links, only: %i[ new create show ], param: :token"
|
|
18
|
+
|
|
19
|
+
if defined?(ActionMailer::Railtie)
|
|
20
|
+
template "app/mailers/magic_links_mailer.rb"
|
|
21
|
+
template "app/views/magic_links_mailer/magic_link.html.erb"
|
|
22
|
+
template "app/views/magic_links_mailer/magic_link.text.erb"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
class MagicLinksController < ApplicationController
|
|
2
|
+
allow_unauthenticated_access
|
|
3
|
+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_magic_link_path, alert: "Try again later." }
|
|
4
|
+
|
|
5
|
+
def new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def create
|
|
9
|
+
user = User.find_by(email_address: params[:email_address])
|
|
10
|
+
user&.send_magic_link
|
|
11
|
+
|
|
12
|
+
redirect_to new_session_path, notice: "Sign-in link sent (if a user with that email address exists)."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
user = User.find_by_valid_magic_link_token(params[:token])
|
|
17
|
+
|
|
18
|
+
if user.nil?
|
|
19
|
+
redirect_to new_magic_link_path, alert: "Sign-in link is invalid or has expired."
|
|
20
|
+
<% if lockable? -%>
|
|
21
|
+
elsif user.locked?
|
|
22
|
+
<% if trackable? -%>
|
|
23
|
+
record_authentication_attempt(user, success: false, failure_reason: "locked")
|
|
24
|
+
<% end -%>
|
|
25
|
+
redirect_to new_session_path, alert: "Your account is locked. Check your email for unlock instructions."
|
|
26
|
+
<% end -%>
|
|
27
|
+
<% if confirmable? -%>
|
|
28
|
+
elsif !user.confirmed?
|
|
29
|
+
<% if trackable? -%>
|
|
30
|
+
record_authentication_attempt(user, success: false, failure_reason: "unconfirmed")
|
|
31
|
+
<% end -%>
|
|
32
|
+
redirect_to new_session_path, alert: "You must confirm your email address before signing in."
|
|
33
|
+
<% end -%>
|
|
34
|
+
<% if invitable? -%>
|
|
35
|
+
elsif user.invitation_pending?
|
|
36
|
+
<% if trackable? -%>
|
|
37
|
+
record_authentication_attempt(user, success: false, failure_reason: "invitation_pending")
|
|
38
|
+
<% end -%>
|
|
39
|
+
redirect_to new_session_path, alert: "You must accept your invitation before signing in."
|
|
40
|
+
<% end -%>
|
|
41
|
+
# Add your own authentication restrictions specific to your application
|
|
42
|
+
# elsif user.deleted_at.present?
|
|
43
|
+
<% if trackable? -%>
|
|
44
|
+
# record_authentication_attempt(user, success: false, failure_reason: "soft_deleted")
|
|
45
|
+
<% end -%>
|
|
46
|
+
# redirect_to new_session_path, alert: "Your account is not available."
|
|
47
|
+
else
|
|
48
|
+
user.consume_magic_link_token!
|
|
49
|
+
<% if lockable? -%>
|
|
50
|
+
user.reset_failed_attempts!
|
|
51
|
+
<% end -%>
|
|
52
|
+
<% if trackable? -%>
|
|
53
|
+
record_authentication_attempt(user, success: true)
|
|
54
|
+
<% end -%>
|
|
55
|
+
start_new_session_for user
|
|
56
|
+
redirect_to after_authentication_url
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
<% if trackable? -%>
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def record_authentication_attempt(user, success:, failure_reason: nil)
|
|
64
|
+
UserAuth.record(user, request, success: success, failure_reason: failure_reason)
|
|
65
|
+
end
|
|
66
|
+
<% end -%>
|
|
67
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module MagicLinkConcern
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
MAGIC_LINK_EXPIRES_IN = 20.minutes
|
|
5
|
+
|
|
6
|
+
class_methods do
|
|
7
|
+
def find_by_valid_magic_link_token(token)
|
|
8
|
+
return if token.blank?
|
|
9
|
+
|
|
10
|
+
user = find_by(magic_link_token: token)
|
|
11
|
+
user if user&.magic_link_sent_at&.after?(MAGIC_LINK_EXPIRES_IN.ago)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def send_magic_link
|
|
16
|
+
generate_magic_link_token!
|
|
17
|
+
MagicLinksMailer.magic_link(self).deliver_later
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def generate_magic_link_token!
|
|
21
|
+
update!(magic_link_token: build_magic_link_token, magic_link_sent_at: Time.current)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Tokens are single-use: consumed on successful sign-in.
|
|
25
|
+
def consume_magic_link_token!
|
|
26
|
+
update!(magic_link_token: nil, magic_link_sent_at: nil)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
protected
|
|
30
|
+
|
|
31
|
+
def build_magic_link_token
|
|
32
|
+
SecureRandom.urlsafe_base64(32)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<h1>Sign in with magic link</h1>
|
|
2
|
+
|
|
3
|
+
<%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
|
|
4
|
+
<%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
|
|
5
|
+
|
|
6
|
+
<%%= form_with url: magic_links_path do |form| %>
|
|
7
|
+
<%%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
|
|
8
|
+
<%%= form.submit "Email me a sign-in link" %>
|
|
9
|
+
<%% end %>
|
|
10
|
+
<br>
|
|
11
|
+
|
|
12
|
+
<%%= link_to "Sign in with password", new_session_path %>
|
data/lib/generators/authentication/templates/app/views/magic_links_mailer/magic_link.html.erb.tt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
You can sign in to your account by following
|
|
3
|
+
<%%= link_to "this sign-in link", magic_link_url(@user.magic_link_token) %>.
|
|
4
|
+
|
|
5
|
+
This link will expire in <%%= distance_of_time_in_words(0, MagicLinkConcern::MAGIC_LINK_EXPIRES_IN) %> and can only be used once.
|
|
6
|
+
</p>
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
<%% end %>
|
|
12
12
|
<br>
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
<% if magic_link? -%>
|
|
15
|
+
<br><%%= link_to "Sign in with magic link", new_magic_link_path %>
|
|
16
|
+
<% end -%>
|
|
17
|
+
<br><%%= link_to "Forgot password?", new_password_path %>
|
|
15
18
|
<% if registerable? -%>
|
|
16
19
|
<br><%%= link_to "Sign up", new_registration_path %>
|
|
17
20
|
<% end -%>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_authentication
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Freerksen
|
|
@@ -37,6 +37,7 @@ files:
|
|
|
37
37
|
- lib/generators/authentication/features/confirmable.rb
|
|
38
38
|
- lib/generators/authentication/features/invitable.rb
|
|
39
39
|
- lib/generators/authentication/features/lockable.rb
|
|
40
|
+
- lib/generators/authentication/features/magic_link.rb
|
|
40
41
|
- lib/generators/authentication/features/recoverable.rb
|
|
41
42
|
- lib/generators/authentication/features/registerable.rb
|
|
42
43
|
- lib/generators/authentication/features/rememberable.rb
|
|
@@ -46,17 +47,20 @@ files:
|
|
|
46
47
|
- lib/generators/authentication/templates/app/controllers/concerns/authentication.rb.tt
|
|
47
48
|
- lib/generators/authentication/templates/app/controllers/confirmations_controller.rb.tt
|
|
48
49
|
- lib/generators/authentication/templates/app/controllers/invitations_controller.rb.tt
|
|
50
|
+
- lib/generators/authentication/templates/app/controllers/magic_links_controller.rb.tt
|
|
49
51
|
- lib/generators/authentication/templates/app/controllers/passwords_controller.rb.tt
|
|
50
52
|
- lib/generators/authentication/templates/app/controllers/registrations_controller.rb.tt
|
|
51
53
|
- lib/generators/authentication/templates/app/controllers/sessions_controller.rb.tt
|
|
52
54
|
- lib/generators/authentication/templates/app/controllers/unlocks_controller.rb.tt
|
|
53
55
|
- lib/generators/authentication/templates/app/mailers/confirmations_mailer.rb.tt
|
|
54
56
|
- lib/generators/authentication/templates/app/mailers/invitations_mailer.rb.tt
|
|
57
|
+
- lib/generators/authentication/templates/app/mailers/magic_links_mailer.rb.tt
|
|
55
58
|
- lib/generators/authentication/templates/app/mailers/passwords_mailer.rb.tt
|
|
56
59
|
- lib/generators/authentication/templates/app/mailers/unlocks_mailer.rb.tt
|
|
57
60
|
- lib/generators/authentication/templates/app/models/concerns/confirmable_concern.rb.tt
|
|
58
61
|
- lib/generators/authentication/templates/app/models/concerns/invitable_concern.rb.tt
|
|
59
62
|
- lib/generators/authentication/templates/app/models/concerns/lockable_concern.rb.tt
|
|
63
|
+
- lib/generators/authentication/templates/app/models/concerns/magic_link_concern.rb.tt
|
|
60
64
|
- lib/generators/authentication/templates/app/models/concerns/recoverable_concern.rb.tt
|
|
61
65
|
- lib/generators/authentication/templates/app/models/concerns/rememberable_concern.rb.tt
|
|
62
66
|
- lib/generators/authentication/templates/app/models/concerns/timeoutable_concern.rb.tt
|
|
@@ -70,6 +74,9 @@ files:
|
|
|
70
74
|
- lib/generators/authentication/templates/app/views/invitations/new.html.erb.tt
|
|
71
75
|
- lib/generators/authentication/templates/app/views/invitations_mailer/invitation_instructions.html.erb.tt
|
|
72
76
|
- lib/generators/authentication/templates/app/views/invitations_mailer/invitation_instructions.text.erb.tt
|
|
77
|
+
- lib/generators/authentication/templates/app/views/magic_links/new.html.erb.tt
|
|
78
|
+
- lib/generators/authentication/templates/app/views/magic_links_mailer/magic_link.html.erb.tt
|
|
79
|
+
- lib/generators/authentication/templates/app/views/magic_links_mailer/magic_link.text.erb.tt
|
|
73
80
|
- lib/generators/authentication/templates/app/views/passwords_mailer/reset.html.erb.tt
|
|
74
81
|
- lib/generators/authentication/templates/app/views/passwords_mailer/reset.text.erb.tt
|
|
75
82
|
- lib/generators/authentication/templates/app/views/registrations/edit.html.erb.tt
|
|
@@ -81,6 +88,7 @@ files:
|
|
|
81
88
|
- lib/generators/authentication/templates/db/migrate/add_confirmable_to_users.rb.tt
|
|
82
89
|
- lib/generators/authentication/templates/db/migrate/add_invitable_to_users.rb.tt
|
|
83
90
|
- lib/generators/authentication/templates/db/migrate/add_lockable_to_users.rb.tt
|
|
91
|
+
- lib/generators/authentication/templates/db/migrate/add_magic_link_to_users.rb.tt
|
|
84
92
|
- lib/generators/authentication/templates/db/migrate/add_recoverable_to_users.rb.tt
|
|
85
93
|
- lib/generators/authentication/templates/db/migrate/add_rememberable_to_users.rb.tt
|
|
86
94
|
- lib/generators/authentication/templates/db/migrate/create_user_auths.rb.tt
|