passwordless 0.7.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb17c0cc08c9dd0062b47d67a470adeee2d0e7dc618e48967180eea61796f47a
4
- data.tar.gz: 870dd7a03e131344f20bdee24ab614e1ef66ff3fe8faf9d4bd05bb08adce032d
3
+ metadata.gz: c45a41b4be30c959c37b50ef8013f4055c97ae516e4b8297b4f56607836cc09b
4
+ data.tar.gz: 7b0c50e087c5be36e5e89476764a407767aee4851fcb38c0b744bd532be8e982
5
5
  SHA512:
6
- metadata.gz: e43144bae67ad300ae753fe131ec6b09fcf997c975e2fcb9a819d457fb0178797d03b4e7278c3c92c4cc18664c54c5231de428be7f6fc63c05a878516e628f7a
7
- data.tar.gz: 5aeca84a612458d6e2dce0b0e9335d80a9f4e4af730540a148c3ef0ed6056417a33b444bfc17c19adf06744a7a2fa38dccb82189206bf2c86354352540b41e7e
6
+ metadata.gz: b35ef690ecd6b37106d785ccb71693606387a8fcc48d5ea115b9d035211b8225d551291d3c64abdb89517cb0ceea17d31807d40ef6f168a9f5df742fd27b8d65
7
+ data.tar.gz: 8d8ba39b3711905ea36f5f20c009a26b42bcea43ed1bd1185ac56c0053e7afdb6fdf786f123fa8c01b2c095ec3104637d5017791056960293869053ff0b248a3
data/README.md CHANGED
@@ -14,16 +14,19 @@ Add authentication to your Rails app without all the icky-ness of passwords.
14
14
 
15
15
  * [Installation](#installation)
16
16
  * [Usage](#usage)
17
- * [Getting the current user, restricting access, the usual](#getting-the-current-user-restricting-access-the-usual)
18
- * [Providing your own templates](#providing-your-own-templates)
19
- * [Overrides](#overrides)
20
- * [Registering new users](#registering-new-users)
21
- * [Generating tokens](#generating-tokens)
22
- * [Token and Session Expiry](#token-and-session-expiry)
23
- * [Redirecting back after sign-in](#redirecting-back-after-sign-in)
24
- * [URLs and links](#urls-and-links)
25
- * [Customize the way to send magic link](#customize-the-way-to-send-magic-link)
26
- * [E-mail security](#e-mail-security)
17
+ * [Getting the current user, restricting access, the usual](#getting-the-current-user-restricting-access-the-usual)
18
+ * [Providing your own templates](#providing-your-own-templates)
19
+ * [Registering new users](#registering-new-users)
20
+ * [URLs and links](#urls-and-links)
21
+ * [Customize the way to send magic link](#customize-the-way-to-send-magic-link)
22
+ * [Generate your own magic links](#generate-your-own-magic-links)
23
+ * [Overrides](#overrides)
24
+ * [Configuration](#configuration)
25
+ * [Customising token generation](#generating-tokens)
26
+ * [Token and Session Expiry](#token-and-session-expiry)
27
+ * [Redirecting back after sign-in](#redirecting-back-after-sign-in)
28
+ * [Claiming tokens](#claiming-tokens)
29
+ * [E-mail security](#e-mail-security)
27
30
  * [License](#license)
28
31
 
29
32
  ## Installation
@@ -43,7 +46,7 @@ $ bin/rails passwordless:install:migrations
43
46
 
44
47
  ## Usage
45
48
 
46
- Passwordless creates a single model called `Passwordless::Session`. It doesn't come with its own `User` model, it expects you to create one, eg.:
49
+ Passwordless creates a single model called `Passwordless::Session`. It doesn't come with its own `User` model, it expects you to create one:
47
50
 
48
51
  ```
49
52
  $ bin/rails generate model User email
@@ -71,7 +74,7 @@ end
71
74
 
72
75
  ### Getting the current user, restricting access, the usual
73
76
 
74
- Passwordless doesn't give you `current_user` automatically -- it's dead easy to add it though:
77
+ Passwordless doesn't give you `current_user` automatically. Here's how you could add it:
75
78
 
76
79
  ```ruby
77
80
  class ApplicationController < ActionController::Base
@@ -84,12 +87,12 @@ class ApplicationController < ActionController::Base
84
87
  private
85
88
 
86
89
  def current_user
87
- @current_user ||= authenticate_by_cookie(User)
90
+ @current_user ||= authenticate_by_session(User)
88
91
  end
89
92
 
90
93
  def require_user!
91
94
  return if current_user
92
- redirect_to root_path, flash: {error: 'You are not worthy!'}
95
+ redirect_to root_path, flash: { error: 'You are not worthy!' }
93
96
  end
94
97
  end
95
98
  ```
@@ -119,39 +122,32 @@ app/views/passwordless/sessions/create.html.erb
119
122
  app/views/passwordless/mailer/magic_link.text.erb
120
123
  ```
121
124
 
122
- See [the bundled views](https://github.com/mikker/passwordless/tree/master/app/views/passwordless).
123
-
124
- ### Overrides
125
-
126
- By default `passwordless` uses the `passwordless_with` column you specify in the model to case insensitively fetch the resource during authentication. You can override this and provide your own customer fetcher by defining a class method `fetch_resource_for_passwordless` in your passwordless model. The method will be supplied with the downcased email and should return an `ActiveRecord` instance of the model.
127
-
128
- Example time:
129
-
130
- Let's say we would like to fetch the record and if it doesn't exist, create automatically.
131
-
132
- ```ruby
133
- class User < ApplicationRecord
134
- def self.fetch_resource_for_passwordless(email)
135
- find_or_create_by(email: email)
136
- end
137
- end
125
+ If you'd like to let the user know whether or not a record was found, `@resource` is provided to the view. You may override `app/views/passwordless/session/create.html.erb` for example like so:
126
+ ```erb
127
+ <% if @resource.present? %>
128
+ <p>User found, check your inbox</p>
129
+ <% else %>
130
+ <p>No user found with the provided email address</p>
131
+ <% end %>
138
132
  ```
139
133
 
134
+ See [the bundled views](https://github.com/mikker/passwordless/tree/master/app/views/passwordless).
135
+
140
136
  ### Registering new users
141
137
 
142
- Because your `User` record is like any other record, you create one like you normally would. Passwordless provides a helper method you can use to sign in the created user after it is saved like so:
138
+ Because your `User` record is like any other record, you create one like you normally would. Passwordless provides a helper method to sign in the created user after it is saved like so:
143
139
 
144
140
  ```ruby
145
141
  class UsersController < ApplicationController
146
142
  include Passwordless::ControllerHelpers # <-- This!
147
- # (unless you already have it in your ApplicationController)
143
+ # (unless you already have it in your ApplicationController)
148
144
 
149
145
  def create
150
146
  @user = User.new user_params
151
147
 
152
148
  if @user.save
153
- sign_in @user # <-- And this!
154
- redirect_to @user, flash: {notice: 'Welcome!'}
149
+ sign_in @user # <-- This!
150
+ redirect_to @user, flash: { notice: 'Welcome!' }
155
151
  else
156
152
  render :new
157
153
  end
@@ -161,9 +157,102 @@ class UsersController < ApplicationController
161
157
  end
162
158
  ```
163
159
 
164
- ### Generating tokens
160
+ ### URLs and links
165
161
 
166
- By default Passwordless generates tokens using Rails' `SecureRandom.urlsafe_base64` but you can change that by setting `Passwordless.token_generator` to something else that responds to `call(session)` eg.:
162
+ By default, Passwordless uses the resource name given to `passwordless_for` to generate its routes and helpers.
163
+
164
+ ```ruby
165
+ passwordless_for :users
166
+ # <%= users.sign_in_path %> # => /users/sign_in
167
+
168
+ passwordless_for :users, at: '/', as: :auth
169
+ # <%= auth.sign_in_path %> # => /sign_in
170
+ ```
171
+
172
+ Also be sure to [specify ActionMailer's `default_url_options.host`](http://guides.rubyonrails.org/action_mailer_basics.html#generating-urls-in-action-mailer-views).
173
+
174
+ ### Customize the way to send magic link
175
+
176
+ By default, magic link will send by email. You can customize this method. For example, you can send magic link via SMS.
177
+
178
+ config/initializers/passwordless.rb
179
+
180
+ ```
181
+ Passwordless.after_session_save = lambda do |session, request|
182
+ # Default behavior is
183
+ # Passwordless::Mailer.magic_link(session).deliver_now
184
+
185
+ # You can change behavior to do something with session model. For example,
186
+ # session.authenticatable.send_sms
187
+ end
188
+ ```
189
+
190
+ You can access user model through authenticatable.
191
+
192
+ ### Generate your own magic links
193
+
194
+ Currently there is not an officially supported way to generate your own magic links to send in your own mailers.
195
+
196
+ However, you can accomplish this with the following snippet of code.
197
+
198
+ ```
199
+ session = Passwordless::Session.new({
200
+ authenticatable: @manager,
201
+ user_agent: 'Command Line',
202
+ remote_addr: 'unknown',
203
+ })
204
+ session.save!
205
+ @magic_link = send(Passwordless.mounted_as).token_sign_in_url(session.token)
206
+ ```
207
+
208
+ You can further customize this URL by specifying the destination path to be redirected to after the user has logged in. You can do this by adding the `destination_path` query parameter to the end of the URL. For example
209
+ ```
210
+ @magic_link = "#{@magic_link}?destination_path=/your-custom-path"
211
+ ```
212
+
213
+ ### Overrides
214
+
215
+ By default `passwordless` uses the `passwordless_with` column to _case insensitively_ fetch the resource.
216
+
217
+ You can override this and provide your own customer fetcher by defining a class method `fetch_resource_for_passwordless` in your passwordless model. The method will be called with the downcased email and should return an `ActiveRecord` instance of the model.
218
+
219
+ Example time:
220
+
221
+ Let's say we would like to fetch the record and if it doesn't exist, create automatically.
222
+
223
+ ```ruby
224
+ class User < ApplicationRecord
225
+ def self.fetch_resource_for_passwordless(email)
226
+ find_or_create_by(email: email)
227
+ end
228
+ end
229
+ ```
230
+
231
+ ## Configuration
232
+
233
+ The following configuration parameters are supported. You can override these for example in `initializers/passwordless.rb`.
234
+
235
+ The default values are shown below. It's recommended to only include the ones that you specifically want to override.
236
+
237
+ ```ruby
238
+ Passwordless.default_from_address = "CHANGE_ME@example.com"
239
+ Passwordless.parent_mailer = "ActionMailer::Base"
240
+ Passwordless.token_generator = Passwordless::UrlSafeBase64Generator.new # Used to generate magic link tokens.
241
+ Passwordless.restrict_token_reuse = false # By default a magic link token can be used multiple times.
242
+ Passwordless.redirect_back_after_sign_in = true # When enabled the user will be redirected to their previous page, or a page specified by the `destination_path` query parameter, if available.
243
+
244
+ Passwordless.expires_at = lambda { 1.year.from_now } # How long until a passwordless session expires.
245
+ Passwordless.timeout_at = lambda { 1.hour.from_now } # How long until a magic link expires.
246
+
247
+ # Default redirection paths
248
+ Passwordless.success_redirect_path = '/' # When a user succeeds in logging in.
249
+ Passwordless.failure_redirect_path = '/' # When a a login is failed for any reason.
250
+ Passwordless.sign_out_redirect_path = '/' # When a user logs out.
251
+ ```
252
+
253
+ ### Customizing token generation
254
+
255
+ By default Passwordless generates tokens using `SecureRandom.urlsafe_base64` but you can change that by setting `Passwordless.token_generator` to something else that responds to `call(session)` eg.:
167
256
 
168
257
  ```ruby
169
258
  Passwordless.token_generator = -> (session) {
@@ -219,41 +308,39 @@ end
219
308
 
220
309
  This can be turned off with `Passwordless.redirect_back_after_sign_in = false` but if you just don't save the previous destination, you'll be fine.
221
310
 
222
- ### URLs and links
311
+ ### Claiming tokens
223
312
 
224
- By default, Passwordless uses the resource name given to `passwordless_for` to generate its routes and helpers.
313
+ Opt-in for marking tokens as `claimed` so they can only be used once.
225
314
 
226
- ```ruby
227
- passwordless_for :users
228
- # <%= users.sign_in_path %> # => /users/sign_in
315
+ config/initializers/passwordless.rb
229
316
 
230
- passwordless_for :users, at: '/', as: :auth
231
- # <%= auth.sign_in_path %> # => /sign_in
317
+ ```ruby
318
+ # Default is `false`
319
+ Passwordless.restrict_token_reuse = true
232
320
  ```
233
321
 
234
- Also be sure to [specify ActionMailer's `default_url_options.host`](http://guides.rubyonrails.org/action_mailer_basics.html#generating-urls-in-action-mailer-views).
235
-
236
-
237
- ### Customize the way to send magic link
322
+ #### Upgrading an existing Rails app
238
323
 
239
- By default, magic link will send by email. You can customize this method. For example, you can send magic link via SMS.
324
+ The simplest way to update your sessions table is with a single migration:
240
325
 
241
- config/initializers/passwordless.rb
326
+ <details>
327
+ <summary>Example migration</summary>
242
328
 
329
+ ```bash
330
+ bin/rails generate migration add_claimed_at_to_passwordless_sessions
243
331
  ```
244
- Passwordless.after_session_save = lambda do |session|
245
- # Default behavior is
246
- # Mailer.magic_link(session).deliver_now
247
332
 
248
- # You can change behavior to do something with session model. For example,
249
- # session.authenticatable.send_sms
333
+ ```ruby
334
+ class AddClaimedAtToPasswordlessSessions < ActiveRecord::Migration[5.2]
335
+ def change
336
+ add_column :passwordless_sessions, :claimed_at, :datetime
337
+ end
250
338
  end
251
- ```
252
-
253
- You can access user model through authenticatable.
254
339
 
340
+ ```
341
+ </details>
255
342
 
256
- ### E-mail security
343
+ ## E-mail security
257
344
 
258
345
  There's no reason that this approach should be less secure than the usual username/password combo. In fact this is most often a more secure option, as users don't get to choose the weak passwords they still use. In a way this is just the same as having each user go through "Forgot password" on every login.
259
346
 
@@ -261,6 +348,10 @@ But be aware that when everyone authenticates via emails you send, the way you s
261
348
 
262
349
  Ideally you should set up your email provider to not log these mails. And be sure to turn on 2-factor auth if your provider supports it.
263
350
 
351
+ # Alternatives
352
+
353
+ - [OTP JWT](https://github.com/stas/otp-jwt) -- Passwordless JSON Web Tokens
354
+
264
355
  # License
265
356
 
266
357
  MIT
@@ -5,9 +5,6 @@ require "bcrypt"
5
5
  module Passwordless
6
6
  # Controller for managing Passwordless sessions
7
7
  class SessionsController < ApplicationController
8
- # Raise this exception when a session is expired.
9
- class SessionTimedOutError < StandardError; end
10
-
11
8
  include ControllerHelpers
12
9
 
13
10
  # get '/sign_in'
@@ -23,10 +20,15 @@ module Passwordless
23
20
  # renders sessions/create.html.erb.
24
21
  # @see Mailer#magic_link Mailer#magic_link
25
22
  def create
26
- session = build_passwordless_session(find_authenticatable)
23
+ @resource = find_authenticatable
24
+ session = build_passwordless_session(@resource)
27
25
 
28
26
  if session.save
29
- Passwordless.after_session_save.call(session)
27
+ if Passwordless.after_session_save.arity == 2
28
+ Passwordless.after_session_save.call(session, request)
29
+ else
30
+ Passwordless.after_session_save.call(session)
31
+ end
30
32
  end
31
33
 
32
34
  render
@@ -41,23 +43,15 @@ module Passwordless
41
43
  def show
42
44
  # Make it "slow" on purpose to make brute-force attacks more of a hassle
43
45
  BCrypt::Password.create(params[:token])
46
+ sign_in passwordless_session
44
47
 
45
- session = find_session
46
- raise SessionTimedOutError if session.timed_out?
47
-
48
- sign_in session.authenticatable
49
-
50
- redirect_enabled = Passwordless.redirect_back_after_sign_in
51
- destination = reset_passwordless_redirect_location!(User)
52
-
53
- if redirect_enabled && destination
54
- redirect_to destination
55
- else
56
- redirect_to main_app.root_path
57
- end
58
- rescue SessionTimedOutError
48
+ redirect_to passwordless_success_redirect_path
49
+ rescue Errors::TokenAlreadyClaimedError
50
+ flash[:error] = I18n.t(".passwordless.sessions.create.token_claimed")
51
+ redirect_to passwordless_failure_redirect_path
52
+ rescue Errors::SessionTimedOutError
59
53
  flash[:error] = I18n.t(".passwordless.sessions.create.session_expired")
60
- redirect_to main_app.root_path
54
+ redirect_to passwordless_failure_redirect_path
61
55
  end
62
56
 
63
57
  # match '/sign_out', via: %i[get delete].
@@ -65,7 +59,31 @@ module Passwordless
65
59
  # @see ControllerHelpers#sign_out
66
60
  def destroy
67
61
  sign_out authenticatable_class
68
- redirect_to main_app.root_path
62
+ redirect_to passwordless_sign_out_redirect_path
63
+ end
64
+
65
+ protected
66
+
67
+ def passwordless_sign_out_redirect_path
68
+ Passwordless.sign_out_redirect_path
69
+ end
70
+
71
+ def passwordless_failure_redirect_path
72
+ Passwordless.failure_redirect_path
73
+ end
74
+
75
+ def passwordless_query_redirect_path
76
+ query_redirect_uri = URI(params[:destination_path])
77
+ query_redirect_uri.to_s if query_redirect_uri.host.nil? || query_redirect_uri.host == URI(request.url).host
78
+ rescue URI::InvalidURIError, ArgumentError
79
+ nil
80
+ end
81
+
82
+ def passwordless_success_redirect_path
83
+ return Passwordless.success_redirect_path unless Passwordless.redirect_back_after_sign_in
84
+
85
+ session_redirect_url = reset_passwordless_redirect_location!(authenticatable_class)
86
+ passwordless_query_redirect_path || session_redirect_url || Passwordless.success_redirect_path
69
87
  end
70
88
 
71
89
  private
@@ -87,19 +105,17 @@ module Passwordless
87
105
  end
88
106
 
89
107
  def find_authenticatable
90
- email = params[:passwordless][email_field].downcase
108
+ email = params[:passwordless][email_field].downcase.strip
91
109
 
92
110
  if authenticatable_class.respond_to?(:fetch_resource_for_passwordless)
93
111
  authenticatable_class.fetch_resource_for_passwordless(email)
94
112
  else
95
- authenticatable_class.where(
96
- "lower(#{email_field}) = ?", params[:passwordless][email_field].downcase
97
- ).first
113
+ authenticatable_class.where("lower(#{email_field}) = ?", email).first
98
114
  end
99
115
  end
100
116
 
101
- def find_session
102
- Session.find_by!(
117
+ def passwordless_session
118
+ @passwordless_session ||= Session.find_by!(
103
119
  authenticatable_type: authenticatable_classname,
104
120
  token: params[:token]
105
121
  )
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Passwordless
4
4
  # The mailer responsible for sending Passwordless' mails.
5
- class Mailer < ActionMailer::Base
5
+ class Mailer < Passwordless.parent_mailer.constantize
6
6
  default from: Passwordless.default_from_address
7
7
 
8
8
  # Sends a magic link (secret token) email.
@@ -11,7 +11,7 @@ module Passwordless
11
11
  @session = session
12
12
 
13
13
  @magic_link = send(Passwordless.mounted_as)
14
- .token_sign_in_url(session.token)
14
+ .token_sign_in_url(session.token)
15
15
 
16
16
  email_field = @session.authenticatable.class.passwordless_email_field
17
17
  mail(
@@ -18,10 +18,17 @@ module Passwordless
18
18
 
19
19
  before_validation :set_defaults
20
20
 
21
- scope :valid, lambda {
22
- where("timeout_at > ?", Time.current)
21
+ scope :available, lambda {
22
+ where("expires_at > ?", Time.current)
23
23
  }
24
24
 
25
+ def self.valid
26
+ available
27
+ end
28
+ class << self
29
+ deprecate :valid, deprecator: SessionValidDeprecation
30
+ end
31
+
25
32
  def expired?
26
33
  expires_at <= Time.current
27
34
  end
@@ -30,6 +37,19 @@ module Passwordless
30
37
  timeout_at <= Time.current
31
38
  end
32
39
 
40
+ def claim!
41
+ raise Errors::TokenAlreadyClaimedError if claimed?
42
+ touch(:claimed_at)
43
+ end
44
+
45
+ def claimed?
46
+ !!claimed_at
47
+ end
48
+
49
+ def available?
50
+ !expired?
51
+ end
52
+
33
53
  private
34
54
 
35
55
  def set_defaults
@@ -5,8 +5,9 @@ en:
5
5
  create:
6
6
  session_expired: 'Your session has expired, please sign in again.'
7
7
  email_sent_if_record_found: "If we found you in the system, we've sent you an email."
8
+ token_claimed: "This link has already been used, try requesting the link again"
8
9
  new:
9
10
  submit: 'Send magic link'
10
11
  mailer:
11
- subject: "Your magic link ✨'"
12
+ subject: "Your magic link ✨"
12
13
  magic_link: "Here's your link: %{link}"
@@ -10,6 +10,7 @@ class CreatePasswordlessSessions < ActiveRecord::Migration[5.1]
10
10
  )
11
11
  t.datetime :timeout_at, null: false
12
12
  t.datetime :expires_at, null: false
13
+ t.datetime :claimed_at
13
14
  t.text :user_agent, null: false
14
15
  t.string :remote_addr, null: false
15
16
  t.string :token, null: false
@@ -1,17 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support"
4
+ require "passwordless/errors"
3
5
  require "passwordless/engine"
4
6
  require "passwordless/url_safe_base_64_generator"
5
7
 
6
8
  # The main Passwordless module
7
9
  module Passwordless
10
+ mattr_accessor(:parent_mailer) { "ActionMailer::Base" }
8
11
  mattr_accessor(:default_from_address) { "CHANGE_ME@example.com" }
9
12
  mattr_accessor(:token_generator) { UrlSafeBase64Generator.new }
13
+ mattr_accessor(:restrict_token_reuse) { false }
10
14
  mattr_accessor(:redirect_back_after_sign_in) { true }
11
15
  mattr_accessor(:mounted_as) { :configured_when_mounting_passwordless }
12
16
 
13
17
  mattr_accessor(:expires_at) { lambda { 1.year.from_now } }
14
18
  mattr_accessor(:timeout_at) { lambda { 1.hour.from_now } }
19
+ mattr_accessor(:success_redirect_path) { "/" }
20
+ mattr_accessor(:failure_redirect_path) { "/" }
21
+ mattr_accessor(:sign_out_redirect_path) { "/" }
15
22
 
16
- mattr_accessor(:after_session_save) { lambda { |session| Mailer.magic_link(session).deliver_now } }
23
+ mattr_accessor(:after_session_save) do
24
+ lambda { |session, _request| Mailer.magic_link(session).deliver_now }
25
+ end
26
+
27
+ CookieDeprecation = ActiveSupport::Deprecation.new("0.9", "passwordless")
28
+ SessionValidDeprecation = ActiveSupport::Deprecation.new("0.9", "passwordless")
17
29
  end
@@ -3,6 +3,12 @@
3
3
  module Passwordless
4
4
  # Helpers to work with Passwordless sessions from controllers
5
5
  module ControllerHelpers
6
+ # Returns the {Passwordless::Session} (if set) from the session.
7
+ # @return [Session, nil]
8
+ def find_passwordless_session_for(authenticatable_class)
9
+ Passwordless::Session.find_by(id: session[session_key(authenticatable_class)])
10
+ end
11
+
6
12
  # Build a new Passwordless::Session from an _authenticatable_ record.
7
13
  # Set's `user_agent` and `remote_addr` from Rails' `request`.
8
14
  # @param authenticatable [ActiveRecord::Base] Instance of an
@@ -17,6 +23,7 @@ module Passwordless
17
23
  end
18
24
  end
19
25
 
26
+ # @deprecated Use {ControllerHelpers#authenticate_by_session}
20
27
  # Authenticate a record using cookies. Looks for a cookie corresponding to
21
28
  # the _authenticatable_class_. If found try to find it in the database.
22
29
  # @param authenticatable_class [ActiveRecord::Base] any Model connected to
@@ -27,54 +34,119 @@ module Passwordless
27
34
  def authenticate_by_cookie(authenticatable_class)
28
35
  key = cookie_name(authenticatable_class)
29
36
  authenticatable_id = cookies.encrypted[key]
30
- return unless authenticatable_id
31
37
 
32
- authenticatable_class.find_by(id: authenticatable_id)
38
+ return authenticatable_class.find_by(id: authenticatable_id) if authenticatable_id
39
+
40
+ authenticate_by_session(authenticatable_class)
41
+ end
42
+ deprecate :authenticate_by_cookie, deprecator: CookieDeprecation
43
+
44
+ def upgrade_passwordless_cookie(authenticatable_class)
45
+ key = cookie_name(authenticatable_class)
46
+
47
+ return unless (authenticatable_id = cookies.encrypted[key])
48
+ cookies.encrypted.permanent[key] = {value: nil}
49
+ cookies.delete(key)
50
+
51
+ return unless (record = authenticatable_class.find_by(id: authenticatable_id))
52
+ new_session = build_passwordless_session(record).tap { |s| s.save! }
53
+
54
+ sign_in new_session
55
+
56
+ new_session.authenticatable
57
+ end
58
+
59
+ # Authenticate a record using the session. Looks for a session key corresponding to
60
+ # the _authenticatable_class_. If found try to find it in the database.
61
+ # @param authenticatable_class [ActiveRecord::Base] any Model connected to
62
+ # passwordless. (e.g - _User_ or _Admin_).
63
+ # @return [ActiveRecord::Base|nil] an instance of Model found by id stored
64
+ # in cookies.encrypted or nil if nothing is found.
65
+ # @see ModelHelpers#passwordless_with
66
+ def authenticate_by_session(authenticatable_class)
67
+ return unless find_passwordless_session_for(authenticatable_class)&.available?
68
+ find_passwordless_session_for(authenticatable_class).authenticatable
33
69
  end
34
70
 
35
- # Signs in user by assigning their id to a permanent cookie.
36
- # @param authenticatable [ActiveRecord::Base] Instance of Model to sign in
37
- # (e.g - @user when @user = User.find(id: some_id)).
71
+ # Signs in session
72
+ # @param authenticatable [Passwordless::Session] Instance of {Passwordless::Session}
73
+ # to sign in
38
74
  # @return [ActiveRecord::Base] the record that is passed in.
39
- def sign_in(authenticatable)
40
- key = cookie_name(authenticatable.class)
41
- cookies.encrypted.permanent[key] = {value: authenticatable.id}
42
- authenticatable
75
+ def sign_in(record)
76
+ passwordless_session =
77
+ if record.is_a?(Passwordless::Session)
78
+ record
79
+ else
80
+ warn "Passwordless::ControllerHelpers#sign_in with authenticatable " \
81
+ "(`#{record.class}') is deprecated. Falling back to creating a " \
82
+ "new Passwordless::Session"
83
+ build_passwordless_session(record).tap { |s| s.save! }
84
+ end
85
+
86
+ passwordless_session.claim! if Passwordless.restrict_token_reuse
87
+
88
+ raise Passwordless::Errors::SessionTimedOutError if passwordless_session.timed_out?
89
+
90
+ key = session_key(passwordless_session.authenticatable_type)
91
+ session[key] = passwordless_session.id
92
+
93
+ if record.is_a?(Passwordless::Session)
94
+ passwordless_session
95
+ else
96
+ passwordless_session.authenticatable
97
+ end
43
98
  end
44
99
 
45
- # Signs out user by deleting their encrypted cookie.
46
- # @param (see #authenticate_by_cookie)
100
+ # Signs out user by deleting the session key.
101
+ # @param (see #authenticate_by_session)
47
102
  # @return [boolean] Always true
48
103
  def sign_out(authenticatable_class)
104
+ # Deprecated - cookies
49
105
  key = cookie_name(authenticatable_class)
50
106
  cookies.encrypted.permanent[key] = {value: nil}
51
107
  cookies.delete(key)
108
+ # /deprecated
109
+
110
+ reset_session
52
111
  true
53
112
  end
54
113
 
55
114
  # Saves request.original_url as the redirect location for a
56
115
  # passwordless Model.
57
- # @param (see #authenticate_by_cookie)
116
+ # @param (see #authenticate_by_session)
58
117
  # @return [String] the redirect url that was just saved.
59
118
  def save_passwordless_redirect_location!(authenticatable_class)
60
- session[session_key(authenticatable_class)] = request.original_url
119
+ session[redirect_session_key(authenticatable_class)] = request.original_url
61
120
  end
62
121
 
63
122
  # Resets the redirect_location to root_path by deleting the redirect_url
64
123
  # from session.
65
- # @param (see #authenticate_by_cookie)
124
+ # @param (see #authenticate_by_session)
66
125
  # @return [String, nil] the redirect url that was just deleted,
67
126
  # or nil if no url found for given Model.
68
127
  def reset_passwordless_redirect_location!(authenticatable_class)
69
- session.delete session_key(authenticatable_class)
128
+ session.delete(redirect_session_key(authenticatable_class))
129
+ end
130
+
131
+ def session_key(authenticatable_class)
132
+ :"passwordless_session_id--#{authenticatable_class_parameterized(authenticatable_class)}"
133
+ end
134
+
135
+ def redirect_session_key(authenticatable_class)
136
+ :"passwordless_prev_location--#{authenticatable_class_parameterized(authenticatable_class)}"
70
137
  end
71
138
 
72
139
  private
73
140
 
74
- def session_key(authenticatable_class)
75
- :"passwordless_prev_location--#{authenticatable_class.base_class}"
141
+ def authenticatable_class_parameterized(authenticatable_class)
142
+ if authenticatable_class.is_a?(String)
143
+ authenticatable_class = authenticatable_class.constantize
144
+ end
145
+
146
+ authenticatable_class.base_class.to_s.parameterize
76
147
  end
77
148
 
149
+ # Deprecated
78
150
  def cookie_name(authenticatable_class)
79
151
  :"#{authenticatable_class.base_class.to_s.underscore}_id"
80
152
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Passwordless
4
+ module Errors
5
+ # Raise this exception when a session is expired.
6
+ class SessionTimedOutError < StandardError; end
7
+
8
+ # Raise this exception when the token has been previously claimed
9
+ class TokenAlreadyClaimedError < StandardError; end
10
+ end
11
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Passwordless
4
- VERSION = '0.7.0' # :nodoc:
4
+ VERSION = "0.10.0" # :nodoc:
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: passwordless
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikkel Malmberg
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-06 00:00:00.000000000 Z
11
+ date: 2020-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 1.3.6
47
+ version: 1.4.1
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 1.3.6
54
+ version: 1.4.1
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: yard
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +80,7 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
- description:
83
+ description:
84
84
  email:
85
85
  - mikkel@brnbw.com
86
86
  executables: []
@@ -104,6 +104,7 @@ files:
104
104
  - lib/passwordless.rb
105
105
  - lib/passwordless/controller_helpers.rb
106
106
  - lib/passwordless/engine.rb
107
+ - lib/passwordless/errors.rb
107
108
  - lib/passwordless/model_helpers.rb
108
109
  - lib/passwordless/router_helpers.rb
109
110
  - lib/passwordless/url_safe_base_64_generator.rb
@@ -112,7 +113,7 @@ homepage: https://github.com/mikker/passwordless
112
113
  licenses:
113
114
  - MIT
114
115
  metadata: {}
115
- post_install_message:
116
+ post_install_message:
116
117
  rdoc_options: []
117
118
  require_paths:
118
119
  - lib
@@ -127,8 +128,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
128
  - !ruby/object:Gem::Version
128
129
  version: '0'
129
130
  requirements: []
130
- rubygems_version: 3.0.1
131
- signing_key:
131
+ rubygems_version: 3.1.4
132
+ signing_key:
132
133
  specification_version: 4
133
134
  summary: Add authentication to your app without all the ickyness of passwords.
134
135
  test_files: []