searls-auth 0.1.1 → 0.2.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: c23b1737c22e177cbc5da56d7aab735c39408b87c6954b59b15b5f6fc3553021
4
- data.tar.gz: 9906eabdf2bf3593d993bdfc6a6ac5234f346a9fa4f0c26b773fa4291a80fd3a
3
+ metadata.gz: 608a7738fece38d0fce18f906f68c5d91122aaf2d274cdcf48dba6b770c04069
4
+ data.tar.gz: 76d7b5e89479236705a1cb866d7358016510fa9899dffd2474fe9139cfc688bf
5
5
  SHA512:
6
- metadata.gz: 2cbd987345d290d2648a67817531f571482f56002a71b5a7a0aacae01f89afc93cc799cf248712f3eae72c630623d4915cf0f3d5f8c71e238a740a9701a6bd35
7
- data.tar.gz: a1e03993409a4306dfe443385c993fa6e9d53f2328914311c46996c263212fcbe0b7d45e93d1a7ed092dc45ea4c2eadb149384d11a97dca097240648ee8f052e
6
+ metadata.gz: c23b8352aaeec522b1878bf3b32c721cd14d06ef31b306a23fcb7e8b1ca1b3659a39904409e30c4492724289057b1a4594486e7878d01304b2830a7ed39e3ecf
7
+ data.tar.gz: 387b365eb74882a67258552f0da17d58913d001ea574fcf4d4c2b0c1487aeac939e6431e210c0b8bbb1471c00bfd01591e3f6aa6969520cee7727850a4d8552f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-09-11
4
+
5
+ * Add `auth_methods` configuration with default `[:email_link, :email_otp]`
6
+
3
7
  ## [0.1.1] - 2025-04-27
4
8
 
5
9
  * Improve error message when token generation fails due to a token not being configured on the user model
data/LICENSE.txt CHANGED
@@ -653,7 +653,7 @@ Also add information on how to contact you by electronic and paper mail.
653
653
  If the program does terminal interaction, make it output a short
654
654
  notice like this when it starts in an interactive mode:
655
655
 
656
- Fine Ants Copyright (C) 2016 Justin Searls
656
+ searls-auth (C) 2025 Searls LLC
657
657
  This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
658
658
  This is free software, and you are welcome to redistribute it
659
659
  under certain conditions; type `show c' for details.
data/README.md CHANGED
@@ -82,8 +82,30 @@ end
82
82
  ```
83
83
  As stated in the comment above, you can find each configuration and its default value in the code.
84
84
 
85
+ ### Choose your login methods
86
+
87
+ By default, users can log in either by clicking a magic link or by entering a 6‑digit code they receive via email. This is controlled by the `auth_methods` configuration:
88
+
89
+ ```ruby
90
+ # config/initializers/searls_auth.rb
91
+ Rails.application.config.after_initialize do
92
+ Searls::Auth.configure do |config|
93
+ # Defaults:
94
+ config.auth_methods = [:email_link, :email_otp]
95
+
96
+ # Link-only (no code in emails, no OTP input shown):
97
+ # config.auth_methods = :email_link
98
+
99
+ # Code-only (no link in emails; OTP input shown):
100
+ # config.auth_methods = :email_otp
101
+ end
102
+ end
103
+ ```
104
+
105
+ One reason you might want to disable e-mail OTP is that it exposes your users to [a pretty easy-to-implement man-in-the-middle attack](https://blog.danielh.cc/blog/passwords).
106
+
85
107
  ## Use it
86
108
 
87
109
  Of course, having a user be "logged in" or not doesn't mean anything if your application doesn't do anything with the knowledge. Users that are logged in will have `session[:user_id]` set to the value of the logged-in user's ID. Logged out users won't have anything set to `session[:user_id]`. What you do with that is your job, not this gem. (Wait, after 20 years does this mean I finally understand the difference between authentication and authorization? Better late than never.)
88
110
 
89
- If this is your first rodeo and you just read the previous paragraph and thought, _yeah, but now what?_, check out the tail end of the [example app README](/example/simple_app/README.md), which shows an approach that a lot of apps use.
111
+ If this is your first rodeo and you just read the previous paragraph and thought, _yeah, but now what?_, check out the tail end of the [example app README](/example/simple_app/README.md#5-require-authentication-for-appropriate-actions), which shows an approach that a lot of apps use.
@@ -11,7 +11,12 @@ module Searls
11
11
  user = searls_auth_config.user_finder_by_email.call(params[:email])
12
12
 
13
13
  if user.present?
14
- attach_short_code_to_session!(user)
14
+ if searls_auth_config.auth_methods.include?(:email_otp)
15
+ attach_short_code_to_session!(user)
16
+ else
17
+ clear_short_code_from_session!
18
+ end
19
+
15
20
  EmailsLink.new.email(
16
21
  user:,
17
22
  redirect_path: params[:redirect_path],
@@ -8,13 +8,27 @@ module Searls
8
8
  end
9
9
 
10
10
  def create
11
- auth_method = params[:short_code].present? ? :short_code : :token
11
+ auth_method = params[:short_code].present? ? :email_otp : :email_link
12
+ if auth_method == :email_otp && !searls_auth_config.auth_methods.include?(:email_otp)
13
+ flash[:error] = searls_auth_config.resolve(:flash_error_after_verify_attempt_invalid_link, params)
14
+ return redirect_to searls_auth.login_path(
15
+ redirect_path: params[:redirect_path],
16
+ redirect_subdomain: params[:redirect_subdomain]
17
+ )
18
+ end
19
+ if auth_method == :email_link && !searls_auth_config.auth_methods.include?(:email_link)
20
+ flash[:error] = searls_auth_config.resolve(:flash_error_after_verify_attempt_invalid_link, params)
21
+ return redirect_to searls_auth.login_path(
22
+ redirect_path: params[:redirect_path],
23
+ redirect_subdomain: params[:redirect_subdomain]
24
+ )
25
+ end
12
26
  authenticator = AuthenticatesUser.new
13
27
  result = case auth_method
14
- when :short_code
28
+ when :email_otp
15
29
  log_short_code_verification_attempt!
16
30
  authenticator.authenticate_by_short_code(params[:short_code], session)
17
- when :token
31
+ when :email_link
18
32
  authenticator.authenticate_by_token(params[:token])
19
33
  end
20
34
 
@@ -36,7 +50,7 @@ module Searls
36
50
  redirect_to searls_auth_config.resolve(:default_redirect_path_after_login,
37
51
  result.user, params, request, main_app)
38
52
  end
39
- elsif auth_method == :short_code
53
+ elsif auth_method == :email_otp
40
54
  if result.exceeded_short_code_attempt_limit?
41
55
  clear_short_code_from_session!
42
56
  flash[:error] = searls_auth_config.resolve(
@@ -11,7 +11,7 @@ module Searls
11
11
 
12
12
  mail(
13
13
  to: format_to(@user),
14
- subject: "Your #{searls_auth_helper.rpad(@config.app_name)}login code is #{@short_code}",
14
+ subject: mail_subject,
15
15
  template_path: @config.mail_login_template_path,
16
16
  template_name: @config.mail_login_template_name
17
17
  ) do |format|
@@ -19,6 +19,17 @@ module Searls
19
19
  format.text
20
20
  end
21
21
  end
22
+
23
+ private
24
+
25
+ def mail_subject
26
+ methods = Searls::Auth.config.auth_methods
27
+ if methods.include?(:email_otp) && @short_code.present?
28
+ "Your #{searls_auth_helper.rpad(@config.app_name)}login code is #{@short_code}"
29
+ else
30
+ "Your #{searls_auth_helper.rpad(@config.app_name)}login link"
31
+ end
32
+ end
22
33
  end
23
34
  end
24
35
  end
@@ -6,32 +6,36 @@
6
6
  Hello!
7
7
  <% end %>
8
8
  </p>
9
- <p style="margin-top: 1rem;">
10
- You can click the button below to log in to your account:
11
- <div style="margin-top: 2rem; text-align: center;">
12
- <%= link_to verify_token_url({
13
- token: @token,
14
- redirect_path: @redirect_path,
15
- redirect_subdomain: @redirect_subdomain
16
- }.compact_blank) do %>
17
- <span style="width: 70%; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); display: inline-block; padding-left: 2rem; padding-right: 2rem; padding-top: 1rem; padding-bottom: 1rem; font-size: 1.5rem; font-weight: 700; border-radius: 0.75rem; background-color: <%= @config.email_button_color %>; color: white;">Log in</span>
18
- <% end %>
19
- <div style="margin-top: 0.5rem; font-size: 0.875rem; color: #6b7280;">
20
- (This link will expire in <%= @config.token_expiry_minutes %> minutes.)
9
+ <% if Searls::Auth.config.auth_methods.include?(:email_link) %>
10
+ <p style="margin-top: 1rem;">
11
+ You can log in by clicking this button:
12
+ <div style="margin-top: 2rem; text-align: center;">
13
+ <%= link_to verify_token_url({
14
+ token: @token,
15
+ redirect_path: @redirect_path,
16
+ redirect_subdomain: @redirect_subdomain
17
+ }.compact_blank) do %>
18
+ <span style="width: 70%; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); display: inline-block; padding-left: 2rem; padding-right: 2rem; padding-top: 1rem; padding-bottom: 1rem; font-size: 1.5rem; font-weight: 700; border-radius: 0.75rem; background-color: <%= @config.email_button_color %>; color: white;">Log in</span>
19
+ <% end %>
20
+ <div style="margin-top: 0.5rem; font-size: 0.875rem; color: #6b7280;">
21
+ (This link will expire in <%= @config.token_expiry_minutes %> minutes.)
22
+ </div>
21
23
  </div>
22
- </div>
23
- </p>
24
- <p style="margin-top: 2rem;">
25
- Alternatively, you can type or paste the following code into your browser:
26
- <div style="margin-top: 2rem; text-align: center;">
27
- <span id="one-time-code" style="width: 70%; display: inline-block; padding-left: 2rem; padding-right: 2rem; padding-top: 1rem; padding-bottom: 1rem; font-size: 1.5rem; font-weight: 700; border: 1px solid; border-radius: 0.75rem; background-color: #f3f4f6; border-color: #e5e7eb;">
28
- <%= @short_code %>
29
- </span>
30
- <div style="margin-top: 0.5rem; font-size: 0.875rem; color: #6b7280;">
31
- (So will this code.)
24
+ </p>
25
+ <% end %>
26
+ <% if Searls::Auth.config.auth_methods.include?(:email_otp) && @short_code.present? %>
27
+ <p style="margin-top: 2rem;">
28
+ You can log in by entering this code:
29
+ <div style="margin-top: 2rem; text-align: center;">
30
+ <span id="one-time-code" style="width: 70%; display: inline-block; padding-left: 2rem; padding-right: 2rem; padding-top: 1rem; padding-bottom: 1rem; font-size: 1.5rem; font-weight: 700; border: 1px solid; border-radius: 0.75rem; background-color: #f3f4f6; border-color: #e5e7eb;">
31
+ <%= @short_code %>
32
+ </span>
33
+ <div style="margin-top: 0.5rem; font-size: 0.875rem; color: #6b7280;">
34
+ (So will this code.)
35
+ </div>
32
36
  </div>
33
- </div>
34
- </p>
37
+ </p>
38
+ <% end %>
35
39
  <p style="margin-top: 2rem; font-size: 0.875rem; color: #6b7280;">
36
40
  p.s. Didn't try to log in? You can safely ignore this email. If this keeps happening, please <%= link_to "let us know", "mailto:#{@config.support_email_address}" %>.
37
41
  </p>
@@ -4,17 +4,22 @@ Hi, <%= user_name %>!
4
4
  Hello!
5
5
  <% end %>
6
6
 
7
- You can log in to your <%= searls_auth_helper.rpad(@config.app_name) %>account at this URL:
7
+ <% if Searls::Auth.config.auth_methods.include?(:email_link) %>
8
+ You can log in by visiting this URL for your <%= searls_auth_helper.rpad(@config.app_name) %>account:
8
9
 
9
10
  <%= searls_auth.verify_token_url({
10
11
  token: @token,
11
12
  redirect_path: @redirect_path,
12
13
  redirect_subdomain: @redirect_subdomain
13
14
  }.compact_blank) %>
15
+ <% end %>
16
+
17
+ <% if Searls::Auth.config.auth_methods.include?(:email_otp) && @short_code.present? %>
14
18
 
15
- Alternatively, you can type or paste the following code into your browser:
19
+ You can log in by entering this code:
16
20
 
17
21
  <%= @short_code %>
22
+ <% end %>
18
23
 
19
24
  Didn't try to log in? You can safely ignore this email.<%= " If this keeps
20
25
  happening, please let us know at #{@config.support_email_address}" if @config.support_email_address.present? %>
@@ -1,24 +1,24 @@
1
1
  <h1>Check your email!</h1>
2
+ <% parts = { email_link: "a link", email_otp: "a six-digit code" }.slice(*Searls::Auth.config.auth_methods).values %>
2
3
  <p>
3
- In the next few moments, you should receive an email that will provide you
4
- two ways to log in: a link and a six-digit code that you can enter below.
4
+ In the next few moments, you should receive an email with <%= parts.to_sentence %> to log in.
5
5
  </p>
6
- <%= form_with(url: searls_auth.verify_path, method: :post, data: {
7
- # Don't use turbo on cross-domain redirects
8
- turbo: searls_auth_helper.enable_turbo?
9
- }) do |f| %>
10
- <%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
11
- <%= f.hidden_field :redirect_subdomain, value: params[:redirect_subdomain] %>
12
- <div data-controller="<%= searls_auth_helper.otp_stimulus_controller %>">
13
- <%= f.label :short_code, "Code" %>
14
- <%= f.text_field :short_code,
15
- maxlength: 6,
16
- inputmode: "numeric",
17
- pattern: "\\d{6}",
18
- autocomplete: "one-time-code",
19
- title: "six-digit code that was emailed to you",
20
- data: searls_auth_helper.otp_field_stimulus_data
21
- %>
22
- </div>
23
- <%= f.submit "Log in" %>
6
+
7
+ <% if Searls::Auth.config.auth_methods.include?(:email_otp) %>
8
+ <%= form_with(url: searls_auth.verify_path, method: :post, data: { turbo: searls_auth_helper.enable_turbo? }) do |f| %>
9
+ <%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
10
+ <%= f.hidden_field :redirect_subdomain, value: params[:redirect_subdomain] %>
11
+ <div data-controller="<%= searls_auth_helper.otp_stimulus_controller %>">
12
+ <%= f.label :short_code, "Code" %>
13
+ <%= f.text_field :short_code,
14
+ maxlength: 6,
15
+ inputmode: "numeric",
16
+ pattern: "\\d{6}",
17
+ autocomplete: "one-time-code",
18
+ title: "six-digit code that was emailed to you",
19
+ data: searls_auth_helper.otp_field_stimulus_data
20
+ %>
21
+ </div>
22
+ <%= f.submit "Log in" %>
23
+ <% end %>
24
24
  <% end %>
@@ -1,6 +1,7 @@
1
1
  module Searls
2
2
  module Auth
3
3
  Config = Struct.new(
4
+ :auth_methods, # array of symbols, e.g., [:email_link, :email_otp]
4
5
  # Data setup
5
6
  :user_finder_by_email, # proc(email)
6
7
  :user_finder_by_id, # proc(id)
@@ -53,6 +54,10 @@ module Searls
53
54
  self[option]
54
55
  end
55
56
  end
57
+
58
+ def auth_methods
59
+ Array(self[:auth_methods]).map(&:to_sym)
60
+ end
56
61
  end
57
62
  end
58
63
  end
@@ -4,8 +4,8 @@ module Searls
4
4
  def email(user:, short_code:, redirect_path: nil, redirect_subdomain: nil)
5
5
  LoginLinkMailer.with(
6
6
  user:,
7
- token: generate_token!(user),
8
- short_code:,
7
+ token: (Searls::Auth.config.auth_methods.include?(:email_link) ? generate_token!(user) : nil),
8
+ short_code: (Searls::Auth.config.auth_methods.include?(:email_otp) ? short_code : nil),
9
9
  redirect_path:,
10
10
  redirect_subdomain:
11
11
  ).login_link.deliver_later
@@ -1,5 +1,5 @@
1
1
  module Searls
2
2
  module Auth
3
- VERSION = "0.1.1"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
data/lib/searls/auth.rb CHANGED
@@ -12,6 +12,7 @@ module Searls
12
12
  class Error < StandardError; end
13
13
 
14
14
  DEFAULT_CONFIG = {
15
+ auth_methods: [:email_link, :email_otp],
15
16
  # Data setup
16
17
  user_finder_by_email: ->(email) { User.find_by(email:) },
17
18
  user_finder_by_id: ->(id) { User.find_by(id:) },
data/script/setup CHANGED
@@ -5,10 +5,5 @@ set -e
5
5
  bundle
6
6
 
7
7
  cd example/simple_app
8
- bundle
9
- bin/rake db:setup
10
- export PLAYWRIGHT_CLI_VERSION=$(bundle exec ruby -e 'require "playwright"; puts Playwright::COMPATIBLE_PLAYWRIGHT_VERSION.strip')
11
- PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn add -D "playwright@$PLAYWRIGHT_CLI_VERSION"
12
- yarn run playwright install chromium
13
-
8
+ ./script/setup
14
9
  cd ../..
data/script/test CHANGED
@@ -7,7 +7,7 @@ bundle exec rake
7
7
 
8
8
  echo "-----> Testing example/simple_app"
9
9
  cd example/simple_app
10
- bin/rake
10
+ ./script/test
11
11
  cd ../..
12
12
 
13
13
  echo "-----> Looks good! 🪩"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searls-auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
@@ -84,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
84
  - !ruby/object:Gem::Version
85
85
  version: '0'
86
86
  requirements: []
87
- rubygems_version: 3.6.7
87
+ rubygems_version: 3.6.9
88
88
  specification_version: 4
89
89
  summary: Searls-flavored login for Rails apps
90
90
  test_files: []