passwordless 0.7.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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: []