passwordless 0.10.0 → 0.11.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: c45a41b4be30c959c37b50ef8013f4055c97ae516e4b8297b4f56607836cc09b
4
- data.tar.gz: 7b0c50e087c5be36e5e89476764a407767aee4851fcb38c0b744bd532be8e982
3
+ metadata.gz: bfea8774f73a80e003f7257caa02afc7c1475a87077a4881b2c1e85442ebf8e9
4
+ data.tar.gz: 4882a066aa2ecc18a4a170e697014b5f09b9bde58c32e821eb60944c9fb90b13
5
5
  SHA512:
6
- metadata.gz: b35ef690ecd6b37106d785ccb71693606387a8fcc48d5ea115b9d035211b8225d551291d3c64abdb89517cb0ceea17d31807d40ef6f168a9f5df742fd27b8d65
7
- data.tar.gz: 8d8ba39b3711905ea36f5f20c009a26b42bcea43ed1bd1185ac56c0053e7afdb6fdf786f123fa8c01b2c095ec3104637d5017791056960293869053ff0b248a3
6
+ metadata.gz: f71185082eb25883c1a7778276c244cd8e4ada2ee1c76137b2838c08b40ed8687b1db3eae2ed869f376be762d03d0ad7978c481d73f42c2338be0b1d200cb07c
7
+ data.tar.gz: 4ad367839721156af66ee6a76786acae842636eda04841ce1a92012dcc245ddd20414d00108908cd27190fa01e4636c88c4668012b893700f6e67ce604ca979f
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <br />
5
5
  </p>
6
6
 
7
- [![Travis](https://travis-ci.org/mikker/passwordless.svg?branch=master)](https://travis-ci.org/mikker/passwordless) [![Rubygems](https://img.shields.io/gem/v/passwordless.svg)](https://rubygems.org/gems/passwordless) [![codecov](https://codecov.io/gh/mikker/passwordless/branch/master/graph/badge.svg)](https://codecov.io/gh/mikker/passwordless) [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
7
+ [![CI](https://github.com/mikker/passwordless/actions/workflows/ci.yml/badge.svg)](https://github.com/mikker/passwordless/actions/workflows/ci.yml) [![Rubygems](https://img.shields.io/gem/v/passwordless.svg)](https://rubygems.org/gems/passwordless) [![codecov](https://codecov.io/gh/mikker/passwordless/branch/master/graph/badge.svg)](https://codecov.io/gh/mikker/passwordless)
8
8
 
9
9
  Add authentication to your Rails app without all the icky-ness of passwords.
10
10
 
@@ -26,6 +26,8 @@ Add authentication to your Rails app without all the icky-ness of passwords.
26
26
  * [Token and Session Expiry](#token-and-session-expiry)
27
27
  * [Redirecting back after sign-in](#redirecting-back-after-sign-in)
28
28
  * [Claiming tokens](#claiming-tokens)
29
+ * [Supporting UUID primary keys](#supporting-uuid-primary-keys)
30
+ * [Testing helpers](#testing-helpers)
29
31
  * [E-mail security](#e-mail-security)
30
32
  * [License](#license)
31
33
 
@@ -48,7 +50,7 @@ $ bin/rails passwordless:install:migrations
48
50
 
49
51
  Passwordless creates a single model called `Passwordless::Session`. It doesn't come with its own `User` model, it expects you to create one:
50
52
 
51
- ```
53
+ ```sh
52
54
  $ bin/rails generate model User email
53
55
  ```
54
56
 
@@ -111,7 +113,9 @@ end
111
113
 
112
114
  ### Providing your own templates
113
115
 
114
- Override `passwordless`' bundled views by adding your own. `passwordless` has 2 action views and 1 mailer view:
116
+ Override `passwordless`' bundled views by adding your own. You can manually copy the specific views that you need or copy them to your application with `rails generate passwordless:views`.
117
+
118
+ `passwordless` has 2 action views and 1 mailer view:
115
119
 
116
120
  ```sh
117
121
  # the form where the user inputs their email address
@@ -131,6 +135,8 @@ If you'd like to let the user know whether or not a record was found, `@resource
131
135
  <% end %>
132
136
  ```
133
137
 
138
+ Please note that, from a security standpoint, this is a **bad practice** because you'd be giving information about which users are registered on your system. It is recommended to use a single message similar to the default one: "If we found you in the system, we've sent you an email". The **best practice** is to never expose which emails are registered on your system.
139
+
134
140
  See [the bundled views](https://github.com/mikker/passwordless/tree/master/app/views/passwordless).
135
141
 
136
142
  ### Registering new users
@@ -177,7 +183,7 @@ By default, magic link will send by email. You can customize this method. For ex
177
183
 
178
184
  config/initializers/passwordless.rb
179
185
 
180
- ```
186
+ ```ruby
181
187
  Passwordless.after_session_save = lambda do |session, request|
182
188
  # Default behavior is
183
189
  # Passwordless::Mailer.magic_link(session).deliver_now
@@ -195,7 +201,7 @@ Currently there is not an officially supported way to generate your own magic li
195
201
 
196
202
  However, you can accomplish this with the following snippet of code.
197
203
 
198
- ```
204
+ ```ruby
199
205
  session = Passwordless::Session.new({
200
206
  authenticatable: @manager,
201
207
  user_agent: 'Command Line',
@@ -319,7 +325,7 @@ config/initializers/passwordless.rb
319
325
  Passwordless.restrict_token_reuse = true
320
326
  ```
321
327
 
322
- #### Upgrading an existing Rails app
328
+ #### Upgrading an existing Rails app to use claim token
323
329
 
324
330
  The simplest way to update your sessions table is with a single migration:
325
331
 
@@ -340,11 +346,55 @@ end
340
346
  ```
341
347
  </details>
342
348
 
349
+ ### Supporting UUID primary keys
350
+
351
+ If your `users` table uses UUIDs for its primary keys, you will need to add a migration
352
+ to change the type of `passwordless`' `authenticatable_id` field to match your primary key type (this will also involve dropping and recreating associated indices).
353
+
354
+ Here is an example migration you can use:
355
+ ```ruby
356
+ class SupportUuidInPasswordlessSessions < ActiveRecord::Migration[6.0]
357
+ def change
358
+ remove_index :passwordless_sessions, column: [:authenticatable_type, :authenticatable_id] if index_exists? :authenticatable_type, :authenticatable_id
359
+ remove_column :passwordless_sessions, :authenticatable_id
360
+ add_column :passwordless_sessions, :authenticatable_id, :uuid
361
+ add_index :passwordless_sessions, [:authenticatable_type, :authenticatable_id], name: 'authenticatable'
362
+ end
363
+ end
364
+ ```
365
+
366
+ Alternatively, you can use `add_reference` with `type: :uuid` in your migration (see docs [here](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference)).
367
+
368
+ ## Testing helpers
369
+
370
+ To help with testing, a set of test helpers are provided.
371
+
372
+ If you are using RSpec, add the following line to your `spec/rails_helper.rb` or
373
+ `spec/spec_helper.rb` if `rails_helper.rb` does not exist:
374
+
375
+ ```ruby
376
+ require "passwordless/test_helpers"
377
+ ```
378
+
379
+ If you are using TestUnit, add this line to your `test/test_helper.rb`:
380
+
381
+ ```ruby
382
+ require "passwordless/test_helpers"
383
+ ```
384
+
385
+
386
+ Then in your controller, request, and system tests/specs, you can utilize the following methods:
387
+
388
+ ```ruby
389
+ passwordless_sign_in(user) # signs you in as a user
390
+ passwordless_sign_out # signs out user
391
+ ```
392
+
343
393
  ## E-mail security
344
394
 
345
395
  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.
346
396
 
347
- But be aware that when everyone authenticates via emails you send, the way you send those mails becomes a weak spot. Email services usually provide a log of all the mails you send so if your app's account is compromised, every user in the system is as well. (This is the same for "Forgot password".) [Reddit was compromised](https://thenextweb.com/hardfork/2018/01/05/reddit-bitcoin-cash-hack/) using this method.
397
+ But be aware that when everyone authenticates via emails you send, the way you send those mails becomes a weak spot. Email services usually provide a log of all the mails you send so if your app's account is compromised, every user in the system is as well. (This is the same for "Forgot password".) [Reddit was compromised](https://thenextweb.com/hardfork/2018/01/05/reddit-bitcoin-cash-stolen-hack/) using this method.
348
398
 
349
399
  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.
350
400
 
@@ -29,9 +29,11 @@ module Passwordless
29
29
  else
30
30
  Passwordless.after_session_save.call(session)
31
31
  end
32
- end
33
32
 
34
- render
33
+ render :create, status: :ok
34
+ else
35
+ render :create, status: :unprocessable_entity
36
+ end
35
37
  end
36
38
 
37
39
  # get '/sign_in/:token'
@@ -43,23 +45,23 @@ module Passwordless
43
45
  def show
44
46
  # Make it "slow" on purpose to make brute-force attacks more of a hassle
45
47
  BCrypt::Password.create(params[:token])
46
- sign_in passwordless_session
48
+ sign_in(passwordless_session)
47
49
 
48
- redirect_to passwordless_success_redirect_path
50
+ redirect_to(passwordless_success_redirect_path)
49
51
  rescue Errors::TokenAlreadyClaimedError
50
52
  flash[:error] = I18n.t(".passwordless.sessions.create.token_claimed")
51
- redirect_to passwordless_failure_redirect_path
53
+ redirect_to(passwordless_failure_redirect_path)
52
54
  rescue Errors::SessionTimedOutError
53
55
  flash[:error] = I18n.t(".passwordless.sessions.create.session_expired")
54
- redirect_to passwordless_failure_redirect_path
56
+ redirect_to(passwordless_failure_redirect_path)
55
57
  end
56
58
 
57
59
  # match '/sign_out', via: %i[get delete].
58
60
  # Signs user out. Redirects to root_path
59
61
  # @see ControllerHelpers#sign_out
60
62
  def destroy
61
- sign_out authenticatable_class
62
- redirect_to passwordless_sign_out_redirect_path
63
+ sign_out(authenticatable_class)
64
+ redirect_to(passwordless_sign_out_redirect_path)
63
65
  end
64
66
 
65
67
  protected
@@ -10,8 +10,7 @@ module Passwordless
10
10
  def magic_link(session)
11
11
  @session = session
12
12
 
13
- @magic_link = send(Passwordless.mounted_as)
14
- .token_sign_in_url(session.token)
13
+ @magic_link = send(Passwordless.mounted_as).token_sign_in_url(session.token)
15
14
 
16
15
  email_field = @session.authenticatable.class.passwordless_email_field
17
16
  mail(
@@ -4,10 +4,13 @@ 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
- belongs_to :authenticatable,
8
- polymorphic: true, inverse_of: :passwordless_sessions
7
+ belongs_to(
8
+ :authenticatable,
9
+ polymorphic: true,
10
+ inverse_of: :passwordless_sessions
11
+ )
9
12
 
10
- validates \
13
+ validates(
11
14
  :authenticatable,
12
15
  :timeout_at,
13
16
  :expires_at,
@@ -15,16 +18,19 @@ module Passwordless
15
18
  :remote_addr,
16
19
  :token,
17
20
  presence: true
21
+ )
18
22
 
19
23
  before_validation :set_defaults
20
24
 
21
- scope :available, lambda {
22
- where("expires_at > ?", Time.current)
23
- }
25
+ scope(
26
+ :available,
27
+ lambda { where("expires_at > ?", Time.current) }
28
+ )
24
29
 
25
30
  def self.valid
26
31
  available
27
32
  end
33
+
28
34
  class << self
29
35
  deprecate :valid, deprecator: SessionValidDeprecation
30
36
  end
data/config/routes.rb CHANGED
@@ -1,8 +1,8 @@
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
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
8
  end
@@ -2,18 +2,19 @@
2
2
 
3
3
  class CreatePasswordlessSessions < ActiveRecord::Migration[5.1]
4
4
  def change
5
- create_table :passwordless_sessions do |t|
5
+ create_table(:passwordless_sessions) do |t|
6
6
  t.belongs_to(
7
7
  :authenticatable,
8
8
  polymorphic: true,
9
9
  index: {name: "authenticatable"}
10
10
  )
11
- t.datetime :timeout_at, null: false
12
- t.datetime :expires_at, null: false
13
- t.datetime :claimed_at
14
- t.text :user_agent, null: false
15
- t.string :remote_addr, null: false
16
- t.string :token, null: false
11
+
12
+ t.datetime(:timeout_at, null: false)
13
+ t.datetime(:expires_at, null: false)
14
+ t.datetime(:claimed_at)
15
+ t.text(:user_agent, null: false)
16
+ t.string(:remote_addr, null: false)
17
+ t.string(:token, null: false)
17
18
 
18
19
  t.timestamps
19
20
  end
@@ -0,0 +1,15 @@
1
+ require 'rails/generators'
2
+
3
+ module Passwordless
4
+ module Generators
5
+ class ViewsGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('../../../app/views/passwordless', __dir__)
7
+
8
+ def install
9
+ copy_file 'mailer/magic_link.text.erb', 'app/views/passwordless/mailer/magic_link.text.erb'
10
+ copy_file 'sessions/new.html.erb', 'app/views/passwordless/sessions/new.html.erb'
11
+ copy_file 'sessions/create.html.erb', 'app/views/passwordless/sessions.create.html.erb'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -39,6 +39,7 @@ module Passwordless
39
39
 
40
40
  authenticate_by_session(authenticatable_class)
41
41
  end
42
+
42
43
  deprecate :authenticate_by_cookie, deprecator: CookieDeprecation
43
44
 
44
45
  def upgrade_passwordless_cookie(authenticatable_class)
@@ -51,7 +52,7 @@ module Passwordless
51
52
  return unless (record = authenticatable_class.find_by(id: authenticatable_id))
52
53
  new_session = build_passwordless_session(record).tap { |s| s.save! }
53
54
 
54
- sign_in new_session
55
+ sign_in(new_session)
55
56
 
56
57
  new_session.authenticatable
57
58
  end
@@ -73,20 +74,25 @@ module Passwordless
73
74
  # to sign in
74
75
  # @return [ActiveRecord::Base] the record that is passed in.
75
76
  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 " \
77
+ passwordless_session = if record.is_a?(Passwordless::Session)
78
+ record
79
+ else
80
+ warn(
81
+ "Passwordless::ControllerHelpers#sign_in with authenticatable " \
81
82
  "(`#{record.class}') is deprecated. Falling back to creating a " \
82
83
  "new Passwordless::Session"
83
- build_passwordless_session(record).tap { |s| s.save! }
84
- end
84
+ )
85
+ build_passwordless_session(record).tap { |s| s.save! }
86
+ end
85
87
 
86
88
  passwordless_session.claim! if Passwordless.restrict_token_reuse
87
89
 
88
90
  raise Passwordless::Errors::SessionTimedOutError if passwordless_session.timed_out?
89
91
 
92
+ old_session = session.dup.to_hash
93
+ reset_session
94
+ old_session.each_pair { |k, v| session[k.to_sym] = v }
95
+
90
96
  key = session_key(passwordless_session.authenticatable_type)
91
97
  session[key] = passwordless_session.id
92
98
 
@@ -105,8 +111,8 @@ module Passwordless
105
111
  key = cookie_name(authenticatable_class)
106
112
  cookies.encrypted.permanent[key] = {value: nil}
107
113
  cookies.delete(key)
108
- # /deprecated
109
114
 
115
+ # /deprecated
110
116
  reset_session
111
117
  true
112
118
  end
@@ -7,15 +7,17 @@ module Passwordless
7
7
 
8
8
  config.to_prepare do
9
9
  require "passwordless/router_helpers"
10
+
10
11
  ActionDispatch::Routing::Mapper.include RouterHelpers
11
12
  require "passwordless/model_helpers"
13
+
12
14
  ActiveRecord::Base.extend ModelHelpers
13
15
  require "passwordless/controller_helpers"
16
+
14
17
  end
15
18
 
16
19
  config.before_initialize do |app|
17
- app.config.i18n.load_path +=
18
- Dir[Engine.root.join("config", "locales", "*.yml")]
20
+ app.config.i18n.load_path += Dir[Engine.root.join("config", "locales", "*.yml")]
19
21
  end
20
22
  end
21
23
  end
@@ -3,9 +3,11 @@
3
3
  module Passwordless
4
4
  module Errors
5
5
  # Raise this exception when a session is expired.
6
- class SessionTimedOutError < StandardError; end
6
+ class SessionTimedOutError < StandardError
7
+ end
7
8
 
8
9
  # Raise this exception when the token has been previously claimed
9
- class TokenAlreadyClaimedError < StandardError; end
10
+ class TokenAlreadyClaimedError < StandardError
11
+ end
10
12
  end
11
13
  end
@@ -8,9 +8,11 @@ module Passwordless
8
8
  # field name (e.g. `:email`)
9
9
  # @param field [string] email submitted by user.
10
10
  def passwordless_with(field)
11
- has_many :passwordless_sessions,
11
+ has_many(
12
+ :passwordless_sessions,
12
13
  class_name: "Passwordless::Session",
13
14
  as: :authenticatable
15
+ )
14
16
 
15
17
  define_singleton_method(:passwordless_email_field) { field }
16
18
  end
@@ -20,8 +20,10 @@ module Passwordless
20
20
  mount_at = at || resource.to_s
21
21
  mount_as = as || resource.to_s
22
22
  mount(
23
- Passwordless::Engine, at: mount_at, as: mount_as,
24
- defaults: {authenticatable: resource.to_s.singularize}
23
+ Passwordless::Engine,
24
+ at: mount_at,
25
+ as: mount_as,
26
+ defaults: {authenticatable: resource.to_s.singularize}
25
27
  )
26
28
 
27
29
  Passwordless.mounted_as = mount_as
@@ -0,0 +1,43 @@
1
+ module Passwordless
2
+ module TestHelpers
3
+ module TestCase
4
+ def passwordless_sign_out
5
+ delete Passwordless::Engine.routes.url_helpers.sign_out_path
6
+ follow_redirect!
7
+ end
8
+
9
+ def passwordless_sign_in(resource)
10
+ session = Passwordless::Session.create!(authenticatable: resource, user_agent: "TestAgent", remote_addr: "unknown")
11
+ get Passwordless::Engine.routes.url_helpers.token_sign_in_path(session.token)
12
+ follow_redirect!
13
+ end
14
+ end
15
+
16
+ module SystemTestCase
17
+ def passwordless_sign_out
18
+ visit Passwordless::Engine.routes.url_helpers.sign_out_path
19
+ end
20
+
21
+ def passwordless_sign_in(resource)
22
+ session = Passwordless::Session.create!(authenticatable: resource, user_agent: "TestAgent", remote_addr: "unknown")
23
+ visit Passwordless::Engine.routes.url_helpers.token_sign_in_path(session.token)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ if defined?(ActiveSupport::TestCase)
30
+ ActiveSupport::TestCase.send(:include, ::Passwordless::TestHelpers::TestCase)
31
+ end
32
+
33
+ if defined?(ActionDispatch::SystemTestCase)
34
+ ActionDispatch::SystemTestCase.send(:include, ::Passwordless::TestHelpers::SystemTestCase)
35
+ end
36
+
37
+ if defined?(RSpec)
38
+ RSpec.configure do |config|
39
+ config.include ::Passwordless::TestHelpers::TestCase, type: :request
40
+ config.include ::Passwordless::TestHelpers::TestCase, type: :controller
41
+ config.include ::Passwordless::TestHelpers::SystemTestCase, type: :system
42
+ end
43
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Passwordless
4
- VERSION = "0.10.0" # :nodoc:
4
+ # :nodoc:
5
+ VERSION = "0.11.0"
5
6
  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.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikkel Malmberg
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-07 00:00:00.000000000 Z
11
+ date: 2022-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -101,12 +101,14 @@ files:
101
101
  - config/locales/en.yml
102
102
  - config/routes.rb
103
103
  - db/migrate/20171104221735_create_passwordless_sessions.rb
104
+ - lib/generators/passwordless/views_generator.rb
104
105
  - lib/passwordless.rb
105
106
  - lib/passwordless/controller_helpers.rb
106
107
  - lib/passwordless/engine.rb
107
108
  - lib/passwordless/errors.rb
108
109
  - lib/passwordless/model_helpers.rb
109
110
  - lib/passwordless/router_helpers.rb
111
+ - lib/passwordless/test_helpers.rb
110
112
  - lib/passwordless/url_safe_base_64_generator.rb
111
113
  - lib/passwordless/version.rb
112
114
  homepage: https://github.com/mikker/passwordless
@@ -128,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
130
  - !ruby/object:Gem::Version
129
131
  version: '0'
130
132
  requirements: []
131
- rubygems_version: 3.1.4
133
+ rubygems_version: 3.3.7
132
134
  signing_key:
133
135
  specification_version: 4
134
136
  summary: Add authentication to your app without all the ickyness of passwords.