passwordless 0.12.0 → 1.0.0.beta1
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 +108 -191
- data/Rakefile +7 -7
- data/app/controllers/passwordless/sessions_controller.rb +121 -39
- data/app/mailers/passwordless/mailer.rb +13 -11
- data/app/models/passwordless/session.rb +25 -12
- data/app/views/passwordless/mailer/sign_in.text.erb +1 -0
- data/app/views/passwordless/sessions/new.html.erb +8 -4
- data/app/views/passwordless/sessions/show.html.erb +5 -0
- data/config/locales/en.yml +18 -6
- data/config/routes.rb +0 -4
- data/db/migrate/20171104221735_create_passwordless_sessions.rb +1 -3
- data/lib/generators/passwordless/views_generator.rb +5 -5
- data/lib/passwordless/config.rb +71 -0
- data/lib/passwordless/controller_helpers.rb +32 -70
- data/lib/passwordless/engine.rb +2 -6
- data/lib/passwordless/errors.rb +4 -0
- data/lib/passwordless/router_helpers.rb +24 -10
- data/lib/passwordless/short_token_generator.rb +9 -0
- data/lib/passwordless/test_helpers.rb +19 -9
- data/lib/passwordless/token_digest.rb +18 -0
- data/lib/passwordless/version.rb +1 -1
- data/lib/passwordless.rb +5 -19
- metadata +12 -51
- data/app/views/passwordless/mailer/magic_link.text.erb +0 -1
- data/lib/passwordless/url_safe_base_64_generator.rb +0 -15
@@ -7,71 +7,108 @@ module Passwordless
|
|
7
7
|
class SessionsController < ApplicationController
|
8
8
|
include ControllerHelpers
|
9
9
|
|
10
|
-
|
10
|
+
helper_method :email_field
|
11
|
+
|
12
|
+
# get '/:resource/sign_in'
|
11
13
|
# Assigns an email_field and new Session to be used by new view.
|
12
14
|
# renders sessions/new.html.erb.
|
13
15
|
def new
|
14
|
-
@email_field = email_field
|
15
16
|
@session = Session.new
|
16
17
|
end
|
17
18
|
|
18
|
-
# post '/sign_in'
|
19
|
+
# post '/:resource/sign_in'
|
19
20
|
# Creates a new Session record then sends the magic link
|
20
21
|
# redirects to sign in page with generic flash message.
|
21
|
-
# @see Mailer#magic_link Mailer#magic_link
|
22
22
|
def create
|
23
|
-
@resource = find_authenticatable
|
24
|
-
|
23
|
+
unless @resource = find_authenticatable
|
24
|
+
raise(
|
25
|
+
ActiveRecord::RecordNotFound,
|
26
|
+
"Couldn't find #{authenticatable_type} with email #{passwordless_session_params[email_field]}"
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
@session = build_passwordless_session(@resource)
|
25
31
|
|
26
|
-
if session.save
|
27
|
-
if Passwordless.after_session_save.arity == 2
|
28
|
-
Passwordless.after_session_save.call(session, request)
|
32
|
+
if @session.save
|
33
|
+
if Passwordless.config.after_session_save.arity == 2
|
34
|
+
Passwordless.config.after_session_save.call(@session, request)
|
29
35
|
else
|
30
|
-
Passwordless.after_session_save.call(session)
|
36
|
+
Passwordless.config.after_session_save.call(@session)
|
31
37
|
end
|
38
|
+
|
39
|
+
redirect_to(
|
40
|
+
url_for(id: @session.id, action: "show"),
|
41
|
+
flash: {notice: I18n.t("passwordless.sessions.create.email_sent")}
|
42
|
+
)
|
43
|
+
else
|
44
|
+
flash[:error] = I18n.t("passwordless.sessions.create.error")
|
45
|
+
render(:new, status: :unprocessable_entity)
|
32
46
|
end
|
33
47
|
|
34
|
-
|
35
|
-
|
48
|
+
rescue ActiveRecord::RecordNotFound
|
49
|
+
flash[:error] = I18n.t("passwordless.sessions.create.not_found")
|
50
|
+
render(:new, status: :not_found)
|
36
51
|
end
|
37
52
|
|
38
|
-
# get
|
53
|
+
# get "/:resource/sign_in/:id"
|
54
|
+
# Shows the form for confirming a Session record.
|
55
|
+
# renders sessions/show.html.erb.
|
56
|
+
def show
|
57
|
+
@session = find_session
|
58
|
+
end
|
59
|
+
|
60
|
+
# patch "/:resource/sign_in/:id"
|
61
|
+
# User submits the form for confirming a Session record.
|
39
62
|
# Looks up session record by provided token. Signs in user if a match
|
40
63
|
# is found. Redirects to either the user's original destination
|
41
|
-
# or
|
64
|
+
# or _Passwordless.config.success_redirect_path_.
|
65
|
+
#
|
42
66
|
# @see ControllerHelpers#sign_in
|
43
67
|
# @see ControllerHelpers#save_passwordless_redirect_location!
|
44
|
-
def
|
45
|
-
|
46
|
-
redirect_to_options = Passwordless.redirect_to_response_options.dup
|
47
|
-
BCrypt::Password.create(params[:token])
|
48
|
-
sign_in(passwordless_session)
|
68
|
+
def update
|
69
|
+
@session = find_session
|
49
70
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
71
|
+
artificially_slow_down_brute_force_attacks(passwordless_session_params[:token])
|
72
|
+
|
73
|
+
authenticate_and_sign_in(@session, passwordless_session_params[:token])
|
74
|
+
end
|
75
|
+
|
76
|
+
# get "/:resource/sign_in/:id/:token"
|
77
|
+
# User visits the link sent to them via email.
|
78
|
+
# Looks up session record by provided token. Signs in user if a match
|
79
|
+
# is found. Redirects to either the user's original destination
|
80
|
+
# or _Passwordless.config.success_redirect_path_.
|
81
|
+
#
|
82
|
+
# @see ControllerHelpers#sign_in
|
83
|
+
# @see ControllerHelpers#save_passwordless_redirect_location!
|
84
|
+
def confirm
|
85
|
+
# Some email clients will visit links in emails to check if they are
|
86
|
+
# safe. We don't want to sign in the user in that case.
|
87
|
+
return head(:ok) if request.head?
|
88
|
+
|
89
|
+
@session = find_session
|
90
|
+
|
91
|
+
artificially_slow_down_brute_force_attacks(params[:token])
|
92
|
+
|
93
|
+
authenticate_and_sign_in(@session, params[:token])
|
57
94
|
end
|
58
95
|
|
59
|
-
# match '/sign_out', via: %i[get delete].
|
96
|
+
# match '/:resource/sign_out', via: %i[get delete].
|
60
97
|
# Signs user out. Redirects to root_path
|
61
98
|
# @see ControllerHelpers#sign_out
|
62
99
|
def destroy
|
63
100
|
sign_out(authenticatable_class)
|
64
|
-
redirect_to(passwordless_sign_out_redirect_path, Passwordless.redirect_to_response_options.dup)
|
101
|
+
redirect_to(passwordless_sign_out_redirect_path, Passwordless.config.redirect_to_response_options.dup)
|
65
102
|
end
|
66
103
|
|
67
104
|
protected
|
68
105
|
|
69
106
|
def passwordless_sign_out_redirect_path
|
70
|
-
Passwordless.sign_out_redirect_path
|
107
|
+
Passwordless.config.sign_out_redirect_path
|
71
108
|
end
|
72
109
|
|
73
110
|
def passwordless_failure_redirect_path
|
74
|
-
Passwordless.failure_redirect_path
|
111
|
+
Passwordless.config.failure_redirect_path
|
75
112
|
end
|
76
113
|
|
77
114
|
def passwordless_query_redirect_path
|
@@ -82,32 +119,54 @@ module Passwordless
|
|
82
119
|
end
|
83
120
|
|
84
121
|
def passwordless_success_redirect_path
|
85
|
-
return Passwordless.success_redirect_path unless Passwordless.redirect_back_after_sign_in
|
122
|
+
return Passwordless.config.success_redirect_path unless Passwordless.config.redirect_back_after_sign_in
|
86
123
|
|
87
124
|
session_redirect_url = reset_passwordless_redirect_location!(authenticatable_class)
|
88
|
-
passwordless_query_redirect_path || session_redirect_url || Passwordless.success_redirect_path
|
125
|
+
passwordless_query_redirect_path || session_redirect_url || Passwordless.config.success_redirect_path
|
89
126
|
end
|
90
127
|
|
91
128
|
private
|
92
129
|
|
130
|
+
def artificially_slow_down_brute_force_attacks(token)
|
131
|
+
# Make it "slow" on purpose to make brute-force attacks more of a hassle
|
132
|
+
BCrypt::Password.create(token)
|
133
|
+
end
|
134
|
+
|
135
|
+
def authenticate_and_sign_in(session, token)
|
136
|
+
if session.authenticate(token)
|
137
|
+
sign_in(session)
|
138
|
+
redirect_to(passwordless_success_redirect_path, status: :see_other, **redirect_to_options)
|
139
|
+
else
|
140
|
+
flash[:error] = I18n.t("passwordless.sessions.errors.invalid_token")
|
141
|
+
render(status: :forbidden, action: "show")
|
142
|
+
end
|
143
|
+
|
144
|
+
rescue Errors::TokenAlreadyClaimedError
|
145
|
+
flash[:error] = I18n.t("passwordless.sessions.errors.token_claimed")
|
146
|
+
redirect_to(passwordless_failure_redirect_path, status: :see_other, **redirect_to_options)
|
147
|
+
rescue Errors::SessionTimedOutError
|
148
|
+
flash[:error] = I18n.t("passwordless.sessions.errors.session_expired")
|
149
|
+
redirect_to(passwordless_failure_redirect_path, status: :see_other, **redirect_to_options)
|
150
|
+
end
|
151
|
+
|
93
152
|
def authenticatable
|
94
153
|
params.fetch(:authenticatable)
|
95
154
|
end
|
96
155
|
|
97
|
-
def
|
156
|
+
def authenticatable_type
|
98
157
|
authenticatable.to_s.camelize
|
99
158
|
end
|
100
159
|
|
101
160
|
def authenticatable_class
|
102
|
-
|
161
|
+
authenticatable_type.constantize
|
103
162
|
end
|
104
163
|
|
105
|
-
def
|
106
|
-
|
164
|
+
def find_session
|
165
|
+
Session.find_by!(id: params[:id], authenticatable_type: authenticatable_type)
|
107
166
|
end
|
108
167
|
|
109
168
|
def find_authenticatable
|
110
|
-
email =
|
169
|
+
email = passwordless_session_params[email_field].downcase.strip
|
111
170
|
|
112
171
|
if authenticatable_class.respond_to?(:fetch_resource_for_passwordless)
|
113
172
|
authenticatable_class.fetch_resource_for_passwordless(email)
|
@@ -116,11 +175,34 @@ module Passwordless
|
|
116
175
|
end
|
117
176
|
end
|
118
177
|
|
178
|
+
def email_field
|
179
|
+
authenticatable_class.passwordless_email_field
|
180
|
+
rescue NoMethodError => e
|
181
|
+
raise(
|
182
|
+
MissingEmailFieldError,
|
183
|
+
<<~MSG
|
184
|
+
undefined method `passwordless_email_field' for #{authenticatable_type}
|
185
|
+
|
186
|
+
Remember to add something like `passwordless_with :email` to you model
|
187
|
+
MSG
|
188
|
+
.strip_heredoc,
|
189
|
+
caller[1..-1]
|
190
|
+
)
|
191
|
+
end
|
192
|
+
|
193
|
+
def redirect_to_options
|
194
|
+
@redirect_to_options ||= (Passwordless.config.redirect_to_response_options.dup || {})
|
195
|
+
end
|
196
|
+
|
119
197
|
def passwordless_session
|
120
198
|
@passwordless_session ||= Session.find_by!(
|
121
|
-
|
122
|
-
|
199
|
+
id: params[:id],
|
200
|
+
authenticatable_type: authenticatable_type
|
123
201
|
)
|
124
202
|
end
|
203
|
+
|
204
|
+
def passwordless_session_params
|
205
|
+
params.require(:passwordless).permit(:token, authenticatable_class.passwordless_email_field)
|
206
|
+
end
|
125
207
|
end
|
126
208
|
end
|
@@ -2,20 +2,22 @@
|
|
2
2
|
|
3
3
|
module Passwordless
|
4
4
|
# The mailer responsible for sending Passwordless' mails.
|
5
|
-
class Mailer < Passwordless.parent_mailer.constantize
|
6
|
-
default from: Passwordless.default_from_address
|
5
|
+
class Mailer < Passwordless.config.parent_mailer.constantize
|
6
|
+
default from: Passwordless.config.default_from_address
|
7
7
|
|
8
|
-
# Sends a
|
9
|
-
#
|
10
|
-
|
11
|
-
|
8
|
+
# Sends a token and a magic link
|
9
|
+
#
|
10
|
+
# @param session [Session] An instance of Passwordless::Session
|
11
|
+
# @param token [String] The token in plaintext. Falls back to `session.token` hoping it
|
12
|
+
# is still in memory (optional)
|
13
|
+
def sign_in(session, token = nil)
|
14
|
+
@token = token || session.token
|
15
|
+
@magic_link = send(:"confirm_#{session.authenticatable_type.tableize}_sign_in_url", session, token)
|
16
|
+
email_field = session.authenticatable.class.passwordless_email_field
|
12
17
|
|
13
|
-
@magic_link = send(Passwordless.mounted_as).token_sign_in_url(session.token)
|
14
|
-
|
15
|
-
email_field = @session.authenticatable.class.passwordless_email_field
|
16
18
|
mail(
|
17
|
-
to:
|
18
|
-
subject: I18n.t("passwordless.mailer.subject")
|
19
|
+
to: session.authenticatable.send(email_field),
|
20
|
+
subject: I18n.t("passwordless.mailer.sign_in.subject")
|
19
21
|
)
|
20
22
|
end
|
21
23
|
end
|
@@ -4,6 +4,8 @@ module Passwordless
|
|
4
4
|
# The session responsible for holding the connection between the record
|
5
5
|
# trying to log in and the unique tokens.
|
6
6
|
class Session < ApplicationRecord
|
7
|
+
self.table_name = "passwordless_sessions"
|
8
|
+
|
7
9
|
belongs_to(
|
8
10
|
:authenticatable,
|
9
11
|
polymorphic: true,
|
@@ -14,9 +16,7 @@ module Passwordless
|
|
14
16
|
:authenticatable,
|
15
17
|
:timeout_at,
|
16
18
|
:expires_at,
|
17
|
-
:
|
18
|
-
:remote_addr,
|
19
|
-
:token,
|
19
|
+
:token_digest,
|
20
20
|
presence: true
|
21
21
|
)
|
22
22
|
|
@@ -27,12 +27,17 @@ module Passwordless
|
|
27
27
|
lambda { where("expires_at > ?", Time.current) }
|
28
28
|
)
|
29
29
|
|
30
|
-
|
31
|
-
|
30
|
+
# save the token in memory so we can put it in emails but only save the
|
31
|
+
# hashed version in the database
|
32
|
+
attr_reader :token
|
33
|
+
|
34
|
+
def token=(plaintext)
|
35
|
+
self.token_digest = Passwordless.digest(plaintext)
|
36
|
+
@token = (plaintext)
|
32
37
|
end
|
33
38
|
|
34
|
-
|
35
|
-
|
39
|
+
def authenticate(token)
|
40
|
+
token_digest == Passwordless.digest(token)
|
36
41
|
end
|
37
42
|
|
38
43
|
def expired?
|
@@ -58,12 +63,20 @@ module Passwordless
|
|
58
63
|
|
59
64
|
private
|
60
65
|
|
66
|
+
def token_digest_available?(token_digest)
|
67
|
+
Session.available.where(token_digest: token_digest).none?
|
68
|
+
end
|
69
|
+
|
61
70
|
def set_defaults
|
62
|
-
self.expires_at ||= Passwordless.expires_at.call
|
63
|
-
self.timeout_at ||= Passwordless.timeout_at.call
|
64
|
-
|
65
|
-
|
66
|
-
|
71
|
+
self.expires_at ||= Passwordless.config.expires_at.call
|
72
|
+
self.timeout_at ||= Passwordless.config.timeout_at.call
|
73
|
+
|
74
|
+
return if self.token_digest
|
75
|
+
|
76
|
+
self.token, self.token_digest = loop {
|
77
|
+
token = Passwordless.config.token_generator.call(self)
|
78
|
+
digest = Passwordless.digest(token)
|
79
|
+
break [token, digest] if token_digest_available?(digest)
|
67
80
|
}
|
68
81
|
end
|
69
82
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= t("passwordless.mailer.sign_in.body", token: @token, magic_link: @magic_link) %>
|
@@ -1,5 +1,9 @@
|
|
1
|
-
<%= form_with
|
2
|
-
<% email_field_name = :"passwordless[#{
|
3
|
-
<%=
|
4
|
-
<%=
|
1
|
+
<%= form_with(model: @session, url: url_for(action: 'new'), data: { turbo: 'false' }) do |f| %>
|
2
|
+
<% email_field_name = :"passwordless[#{email_field}]" %>
|
3
|
+
<%= f.label email_field_name, t("passwordless.sessions.new.email.label") %>
|
4
|
+
<%= text_field_tag email_field_name,
|
5
|
+
params.fetch(email_field_name, nil),
|
6
|
+
required: true,
|
7
|
+
placeholder: t("passwordless.sessions.new.email.placeholder") %>
|
8
|
+
<%= f.submit t("passwordless.sessions.new.submit") %>
|
5
9
|
<% end %>
|
data/config/locales/en.yml
CHANGED
@@ -2,12 +2,24 @@
|
|
2
2
|
en:
|
3
3
|
passwordless:
|
4
4
|
sessions:
|
5
|
+
new:
|
6
|
+
email:
|
7
|
+
label: "E-mail address"
|
8
|
+
placeholder: "user@example.com"
|
9
|
+
submit: "Sign in"
|
5
10
|
create:
|
6
|
-
|
7
|
-
|
11
|
+
email_sent: "We've sent you an email with a secret token"
|
12
|
+
not_found: "We couldn't find a user with that email address"
|
13
|
+
error: "An error occured"
|
14
|
+
errors:
|
15
|
+
invalid_token: "Token is invalid"
|
16
|
+
session_expired: "Your session has expired, please sign in again."
|
8
17
|
token_claimed: "This link has already been used, try requesting the link again"
|
9
|
-
new:
|
10
|
-
submit: 'Send magic link'
|
11
18
|
mailer:
|
12
|
-
|
13
|
-
|
19
|
+
sign_in:
|
20
|
+
subject: "Signing in ✨"
|
21
|
+
body: |-
|
22
|
+
Use this token to complete your sign in: %{token}
|
23
|
+
|
24
|
+
Alternatively you can use this link to sign in directly:
|
25
|
+
%{magic_link}
|
data/config/routes.rb
CHANGED
@@ -1,8 +1,4 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
Passwordless::Engine.routes.draw do
|
4
|
-
get("/sign_in", to: "sessions#new", as: :sign_in)
|
5
|
-
post("/sign_in", to: "sessions#create")
|
6
|
-
get("/sign_in/:token", to: "sessions#show", as: :token_sign_in)
|
7
|
-
match("/sign_out", to: "sessions#destroy", via: %i[get delete], as: :sign_out)
|
8
4
|
end
|
@@ -12,9 +12,7 @@ class CreatePasswordlessSessions < ActiveRecord::Migration[5.1]
|
|
12
12
|
t.datetime(:timeout_at, null: false)
|
13
13
|
t.datetime(:expires_at, null: false)
|
14
14
|
t.datetime(:claimed_at)
|
15
|
-
t.
|
16
|
-
t.string(:remote_addr, null: false)
|
17
|
-
t.string(:token, null: false)
|
15
|
+
t.string(:token_digest, null: false)
|
18
16
|
|
19
17
|
t.timestamps
|
20
18
|
end
|
@@ -1,14 +1,14 @@
|
|
1
|
-
require
|
1
|
+
require "rails/generators"
|
2
2
|
|
3
3
|
module Passwordless
|
4
4
|
module Generators
|
5
5
|
class ViewsGenerator < Rails::Generators::Base
|
6
|
-
source_root File.expand_path(
|
6
|
+
source_root File.expand_path("../../../app/views/passwordless", __dir__)
|
7
7
|
|
8
8
|
def install
|
9
|
-
copy_file
|
10
|
-
copy_file
|
11
|
-
copy_file
|
9
|
+
copy_file("mailer/sign_in.text.erb", "app/views/passwordless/mailer/sign_in.text.erb")
|
10
|
+
copy_file("sessions/new.html.erb", "app/views/passwordless/sessions/new.html.erb")
|
11
|
+
copy_file("sessions/show.html.erb", "app/views/passwordless/sessions/show.html.erb")
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "passwordless/short_token_generator"
|
2
|
+
|
3
|
+
module Passwordless
|
4
|
+
module Options
|
5
|
+
module ClassMethods
|
6
|
+
def option(name, default: nil)
|
7
|
+
attr_accessor(name)
|
8
|
+
schema[name] = default
|
9
|
+
end
|
10
|
+
|
11
|
+
def schema
|
12
|
+
@schema ||= {}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_defaults!
|
17
|
+
self.class.schema.each do |name, default|
|
18
|
+
instance_variable_set("@#{name}", default)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.included(cls)
|
23
|
+
cls.extend(ClassMethods)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Configuration
|
28
|
+
include Options
|
29
|
+
|
30
|
+
option :default_from_address, default: "CHANGE_ME@example.com"
|
31
|
+
option :parent_mailer, default: "ActionMailer::Base"
|
32
|
+
option :restrict_token_reuse, default: true
|
33
|
+
option :token_generator, default: ShortTokenGenerator.new
|
34
|
+
|
35
|
+
option :expires_at, default: lambda { 1.year.from_now }
|
36
|
+
option :timeout_at, default: lambda { 10.minutes.from_now }
|
37
|
+
|
38
|
+
option :redirect_back_after_sign_in, default: true
|
39
|
+
option :redirect_to_response_options, default: {}
|
40
|
+
option :success_redirect_path, default: "/"
|
41
|
+
option :failure_redirect_path, default: "/"
|
42
|
+
option :sign_out_redirect_path, default: "/"
|
43
|
+
option(
|
44
|
+
:after_session_save,
|
45
|
+
default: lambda do |session, _request|
|
46
|
+
Mailer.sign_in(session, session.token).deliver_now
|
47
|
+
end
|
48
|
+
)
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
set_defaults!
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module Configurable
|
56
|
+
attr_writer :config
|
57
|
+
|
58
|
+
def config
|
59
|
+
@config ||= Configuration.new
|
60
|
+
end
|
61
|
+
|
62
|
+
def configure
|
63
|
+
yield(config)
|
64
|
+
end
|
65
|
+
|
66
|
+
def reset_config!
|
67
|
+
@config = Configuration.new
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -10,51 +10,33 @@ module Passwordless
|
|
10
10
|
end
|
11
11
|
|
12
12
|
# Build a new Passwordless::Session from an _authenticatable_ record.
|
13
|
-
# Set's `user_agent` and `remote_addr` from Rails' `request`.
|
14
13
|
# @param authenticatable [ActiveRecord::Base] Instance of an
|
15
14
|
# authenticatable Rails model
|
16
15
|
# @return [Session] the new Session object
|
17
16
|
# @see ModelHelpers#passwordless_with
|
18
17
|
def build_passwordless_session(authenticatable)
|
19
|
-
Session.new
|
20
|
-
us.remote_addr = request.remote_addr
|
21
|
-
us.user_agent = request.env["HTTP_USER_AGENT"]
|
22
|
-
us.authenticatable = authenticatable
|
23
|
-
end
|
18
|
+
Session.new(authenticatable: authenticatable)
|
24
19
|
end
|
25
20
|
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# @
|
30
|
-
#
|
31
|
-
# @return [ActiveRecord::Base|nil] an instance of Model found by id stored
|
32
|
-
# in cookies.encrypted or nil if nothing is found.
|
21
|
+
# Create a new Passwordless::Session from an _authenticatable_ record.
|
22
|
+
# @param authenticatable [ActiveRecord::Base] Instance of an
|
23
|
+
# authenticatable Rails model
|
24
|
+
# @return [Session] the new Session object
|
25
|
+
# @raise [ActiveRecord::RecordInvalid] if the Session is invalid
|
33
26
|
# @see ModelHelpers#passwordless_with
|
34
|
-
def
|
35
|
-
|
36
|
-
authenticatable_id = cookies.encrypted[key]
|
37
|
-
|
38
|
-
return authenticatable_class.find_by(id: authenticatable_id) if authenticatable_id
|
39
|
-
|
40
|
-
authenticate_by_session(authenticatable_class)
|
27
|
+
def create_passwordless_session!(authenticatable)
|
28
|
+
Session.create!(authenticatable: authenticatable)
|
41
29
|
end
|
42
30
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
return unless (record = authenticatable_class.find_by(id: authenticatable_id))
|
53
|
-
new_session = build_passwordless_session(record).tap { |s| s.save! }
|
54
|
-
|
55
|
-
sign_in(new_session)
|
56
|
-
|
57
|
-
new_session.authenticatable
|
31
|
+
# Create a new Passwordless::Session from an _authenticatable_ record.
|
32
|
+
# @param authenticatable [ActiveRecord::Base] Instance of an
|
33
|
+
# authenticatable Rails model
|
34
|
+
# @return [Session, nil] the new Session object or nil
|
35
|
+
# @see ModelHelpers#passwordless_with
|
36
|
+
def create_passwordless_session(authenticatable)
|
37
|
+
create_passwordless_session!(authenticatable)
|
38
|
+
rescue ActiveRecord::RecordInvalid
|
39
|
+
nil
|
58
40
|
end
|
59
41
|
|
60
42
|
# Authenticate a record using the session. Looks for a session key corresponding to
|
@@ -65,55 +47,40 @@ module Passwordless
|
|
65
47
|
# in cookies.encrypted or nil if nothing is found.
|
66
48
|
# @see ModelHelpers#passwordless_with
|
67
49
|
def authenticate_by_session(authenticatable_class)
|
68
|
-
|
69
|
-
|
50
|
+
pwless_session = find_passwordless_session_for(authenticatable_class)
|
51
|
+
return unless pwless_session&.available?
|
52
|
+
|
53
|
+
pwless_session.authenticatable
|
70
54
|
end
|
71
55
|
|
72
56
|
# Signs in session
|
73
57
|
# @param authenticatable [Passwordless::Session] Instance of {Passwordless::Session}
|
74
58
|
# to sign in
|
75
59
|
# @return [ActiveRecord::Base] the record that is passed in.
|
76
|
-
def sign_in(
|
77
|
-
passwordless_session
|
78
|
-
record
|
79
|
-
else
|
80
|
-
warn(
|
81
|
-
"Passwordless::ControllerHelpers#sign_in with authenticatable " \
|
82
|
-
"(`#{record.class}') is deprecated. Falling back to creating a " \
|
83
|
-
"new Passwordless::Session"
|
84
|
-
)
|
85
|
-
build_passwordless_session(record).tap { |s| s.save! }
|
86
|
-
end
|
87
|
-
|
88
|
-
passwordless_session.claim! if Passwordless.restrict_token_reuse
|
60
|
+
def sign_in(passwordless_session)
|
61
|
+
passwordless_session.claim! if Passwordless.config.restrict_token_reuse
|
89
62
|
|
90
63
|
raise Passwordless::Errors::SessionTimedOutError if passwordless_session.timed_out?
|
91
64
|
|
92
|
-
|
93
|
-
|
94
|
-
|
65
|
+
if defined?(reset_session)
|
66
|
+
old_session = session.dup.to_hash
|
67
|
+
# allow usage outside controllers
|
68
|
+
reset_session
|
69
|
+
old_session.each_pair { |k, v| session[k.to_sym] = v }
|
70
|
+
end
|
95
71
|
|
96
72
|
key = session_key(passwordless_session.authenticatable_type)
|
97
73
|
session[key] = passwordless_session.id
|
98
74
|
|
99
|
-
|
100
|
-
passwordless_session
|
101
|
-
else
|
102
|
-
passwordless_session.authenticatable
|
103
|
-
end
|
75
|
+
passwordless_session
|
104
76
|
end
|
105
77
|
|
106
78
|
# Signs out user by deleting the session key.
|
107
79
|
# @param (see #authenticate_by_session)
|
108
80
|
# @return [boolean] Always true
|
109
81
|
def sign_out(authenticatable_class)
|
110
|
-
|
111
|
-
|
112
|
-
cookies.encrypted.permanent[key] = {value: nil}
|
113
|
-
cookies.delete(key)
|
114
|
-
|
115
|
-
# /deprecated
|
116
|
-
reset_session if defined?(reset_session) # allow usage outside controllers
|
82
|
+
session.delete(session_key(authenticatable_class))
|
83
|
+
reset_session
|
117
84
|
true
|
118
85
|
end
|
119
86
|
|
@@ -151,10 +118,5 @@ module Passwordless
|
|
151
118
|
|
152
119
|
authenticatable_class.base_class.to_s.parameterize
|
153
120
|
end
|
154
|
-
|
155
|
-
# Deprecated
|
156
|
-
def cookie_name(authenticatable_class)
|
157
|
-
:"#{authenticatable_class.base_class.to_s.underscore}_id"
|
158
|
-
end
|
159
121
|
end
|
160
122
|
end
|