passwordless 0.11.0 → 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +112 -189
- 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 +31 -69
- 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 -18
- metadata +12 -52
- data/app/views/passwordless/mailer/magic_link.text.erb +0 -1
- data/app/views/passwordless/sessions/create.html.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
|
-
# @see Mailer#magic_link Mailer#magic_link
|
21
|
+
# redirects to sign in page with generic flash message.
|
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
|
32
38
|
|
33
|
-
|
39
|
+
redirect_to(
|
40
|
+
url_for(id: @session.id, action: "show"),
|
41
|
+
flash: {notice: I18n.t("passwordless.sessions.create.email_sent")}
|
42
|
+
)
|
34
43
|
else
|
35
|
-
|
44
|
+
flash[:error] = I18n.t("passwordless.sessions.create.error")
|
45
|
+
render(:new, status: :unprocessable_entity)
|
36
46
|
end
|
47
|
+
|
48
|
+
rescue ActiveRecord::RecordNotFound
|
49
|
+
flash[:error] = I18n.t("passwordless.sessions.create.not_found")
|
50
|
+
render(:new, status: :not_found)
|
37
51
|
end
|
38
52
|
|
39
|
-
# 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.
|
40
62
|
# Looks up session record by provided token. Signs in user if a match
|
41
63
|
# is found. Redirects to either the user's original destination
|
42
|
-
# or
|
64
|
+
# or _Passwordless.config.success_redirect_path_.
|
65
|
+
#
|
43
66
|
# @see ControllerHelpers#sign_in
|
44
67
|
# @see ControllerHelpers#save_passwordless_redirect_location!
|
45
|
-
def
|
46
|
-
|
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)
|
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
|
-
<%=
|
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,54 +47,39 @@ 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
|
-
key = cookie_name(authenticatable_class)
|
112
|
-
cookies.encrypted.permanent[key] = {value: nil}
|
113
|
-
cookies.delete(key)
|
114
|
-
|
115
|
-
# /deprecated
|
82
|
+
session.delete(session_key(authenticatable_class))
|
116
83
|
reset_session
|
117
84
|
true
|
118
85
|
end
|
@@ -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
|