door_check 0.3.2 → 0.4.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: e8ca6d62944588e5c6daf6940fea53510118c992ee376e7e08a803740c4c50f4
4
- data.tar.gz: 19191202b55fc41342a1724c8ada168cd336b45c8820c53da271e00c04b9bd2d
3
+ metadata.gz: 809331dc702d546a27776ecd87d0ad8a7f169defb4814ee895a49f1af926c934
4
+ data.tar.gz: e738576ea8756212978c60542adf296319761e95bfd38442f3f4274ccd80858a
5
5
  SHA512:
6
- metadata.gz: 97711a6615bd149c32ef35881b95051a0c7939a1c6c412a94d1964d059b125bd748cf375cc1a232fb349e7fa935186174866d0365e1db9e053af9ab8bfc8ad11
7
- data.tar.gz: 0dacebc7827611237e1fd36a33a6b29d2d42bf1ef02706a23660cffe398dfb8144f953caef27177d1959fe4c7aacf964aa2c0966104510520c213d0ad55f2d39
6
+ metadata.gz: 3a414ec12f431ce25dab37699c546700c9f5c9bbc34dc333bd2cdf092f3ba03e4dfb118709aab29c5ade9f233ba2b535e7ce0798c4564a16bfb38204937bbaf6
7
+ data.tar.gz: b1a90170eeb48463e613b2a680e289cc50b1d4f4781d15e426b2d5adc9a950f3481a584be36ee72c2a7fa197a2398a51137d69c1eb290bdeb7a0665b121780a7
@@ -27,7 +27,7 @@ module DoorCheck
27
27
  proved = door_check_proved_by.map(&:first)
28
28
  proved += [ :trusted_device ] if door_check_trusted_browser?(person)
29
29
  account = door_check_account
30
- PersonRule.for(person).met_by?(proved) && (account.nil? || AccountRule.for(account).met_by?(proved))
30
+ PersonRule.for(person).met_by?(proved, account: account) && (account.nil? || AccountRule.for(account).met_by?(proved))
31
31
  end
32
32
 
33
33
  def door_check_trusted_browser?(person)
@@ -31,7 +31,7 @@ module DoorCheck
31
31
  @person = door_check_person
32
32
  @authenticator = DoorCheck.configuration.authenticator
33
33
  @second_step_set_up = SecondStep.set_up?(@person)
34
- @setups = SecondStep.offered_setups(door_check_account)
34
+ @setups = SecondStep.setups_before_second_step(door_check_account)
35
35
  end
36
36
 
37
37
  def prepare_passkey
@@ -6,8 +6,16 @@ module DoorCheck
6
6
  find_or_initialize_by(person: person)
7
7
  end
8
8
 
9
- def met_by?(method_names)
10
- !second_step_required || method_names.any? { |name| Methods.roles(name).include?(:second_step) }
9
+ def met_by?(method_names, account: nil)
10
+ !in_force_in?(account) || method_names.any? { |name| Methods.roles(name).include?(:second_step) }
11
+ end
12
+
13
+ def in_force_in?(account)
14
+ second_step_required && may_choose_in?(account)
15
+ end
16
+
17
+ def may_choose_in?(account)
18
+ DoorCheck.configuration.people_choose_second_step && (account.nil? || AccountRule.for(account).members_choose_second_step)
11
19
  end
12
20
  end
13
21
  end
@@ -7,8 +7,18 @@ module DoorCheck
7
7
  end
8
8
 
9
9
  def call
10
+ return Result.new(ok: false, message: refusal) unless PersonRule.for(@person).may_choose_in?(@account)
11
+
10
12
  PersonRule.for(@person).update!(second_step_required: @values[:second_step_required] == "1")
11
13
  Result.new(ok: true)
12
14
  end
15
+
16
+ private
17
+
18
+ def refusal
19
+ return "This app decides when you are asked for a second step." unless DoorCheck.configuration.people_choose_second_step
20
+
21
+ "Your account decides whether you are asked for a second step."
22
+ end
13
23
  end
14
24
  end
@@ -8,13 +8,24 @@ module DoorCheck
8
8
 
9
9
  def call
10
10
  return Result.new(ok: false, message: "Pick only methods this app offers.") if picks_unoffered_method?
11
+ return Result.new(ok: false, message: "This app decides when members are asked for a second step.") if sets_forbidden_members_choice?
11
12
 
12
- AccountRule.for(@account).update!(second_step_required: @values[:second_step_required] == "1", accepted_methods: @values[:accepted_methods])
13
+ AccountRule.for(@account).update!(saved_values)
13
14
  Result.new(ok: true)
14
15
  end
15
16
 
16
17
  private
17
18
 
19
+ def saved_values
20
+ saved = { second_step_required: @values[:second_step_required] == "1", accepted_methods: @values[:accepted_methods] }
21
+ saved[:members_choose_second_step] = @values[:members_choose_second_step] == "1" if @values.key?(:members_choose_second_step)
22
+ saved
23
+ end
24
+
25
+ def sets_forbidden_members_choice?
26
+ @values.key?(:members_choose_second_step) && !DoorCheck.configuration.people_choose_second_step
27
+ end
28
+
18
29
  def picks_unoffered_method?
19
30
  picked = Array(@values[:accepted_methods]).map(&:to_sym)
20
31
  (picked - DoorCheck.configuration.offered_methods).any?
@@ -1,14 +1,24 @@
1
1
  module DoorCheck
2
2
  module SecondStep
3
- SETUP_PAGES = { authenticator_app: :new_authenticator_path }.freeze
3
+ Setup = Data.define(:method_name, :label, :page, :request_method, :before_second_step)
4
+
5
+ SETUPS = [
6
+ Setup.new(:authenticator_app, "Set up an authenticator app", :new_authenticator_path, :get, true),
7
+ Setup.new(:passkey, "Add a passkey or hardware key", :new_passkey_path, :get, false),
8
+ Setup.new(:backup_codes, "Make new backup codes", :backup_codes_path, :post, false)
9
+ ].freeze
4
10
 
5
11
  def self.set_up?(person)
6
12
  Credential.exists?(person: person, method_name: %w[authenticator_app backup_codes passkey]) || DoorCheck.configuration.authenticator&.set_up?(person)
7
13
  end
8
14
 
9
- def self.offered_setups(account)
15
+ def self.offered_setups
16
+ SETUPS.select { |setup| DoorCheck.configuration.offered_methods.include?(setup.method_name) }
17
+ end
18
+
19
+ def self.setups_before_second_step(account)
10
20
  rule = account && AccountRule.for(account)
11
- SETUP_PAGES.select { |method_name, _| DoorCheck.configuration.offered_methods.include?(method_name) && (rule.nil? || rule.accepts?(method_name)) }
21
+ offered_setups.select { |setup| setup.before_second_step && (rule.nil? || rule.accepts?(setup.method_name)) }
12
22
  end
13
23
  end
14
24
  end
@@ -21,8 +21,8 @@
21
21
  <% else %>
22
22
  <%= ui_section(title: "Set up a second step", subtitle: "A second step is required here, and you have none set up yet.") do %>
23
23
  <%= ui_grid(gap: :sm) do %>
24
- <% @setups.each do |method_name, setup_page| %>
25
- <%= ui_button(label: "Set up #{method_name.to_s.humanize.downcase}", href: door_check.public_send(setup_page), full_width: true) %>
24
+ <% @setups.each do |setup| %>
25
+ <%= ui_button(label: "Set up #{setup.method_name.to_s.humanize.downcase}", href: door_check.public_send(setup.page), full_width: true) %>
26
26
  <% end %>
27
27
  <% end %>
28
28
  <% end %>
@@ -1,40 +1,65 @@
1
- <%= ui_data_table(items: DoorCheck::Credential.where(person: person),
2
- columns: [ { label: "Method" }, { last_used: "Last used" }, { accepted_by: "Accepted by" } ],
3
- empty_message: "No sign-in methods set up yet.") do |table| %>
4
- <% table.actions do |credential| %>
5
- <%= ui_action_menu_item(label: "Remove", href: "#{submit_urls[:remove]}?credential_id=#{credential.id}", method: :patch,
6
- confirm: "Remove this sign-in method?") %>
1
+ <%= ui_section(title: "Sign-in methods") do %>
2
+ <%= ui_panel do %>
3
+ <%= ui_data_table(items: DoorCheck::Credential.where(person: person),
4
+ columns: [ { label: "Method" }, { last_used: "Last used" }, { accepted_by: "Accepted by" } ],
5
+ empty_message: "No sign-in methods set up yet.", phone_list: true) do |table| %>
6
+ <% table.actions do |credential| %>
7
+ <%= ui_action_menu_item(label: "Remove", href: "#{submit_urls[:remove]}?credential_id=#{credential.id}", method: :patch,
8
+ confirm: "Remove this sign-in method?") %>
9
+ <% end %>
10
+ <% end %>
11
+ <%= ui_section(spacing: :sm) do %>
12
+ <%= ui_grid(cols: { default: 1, sm: 3 }, gap: :sm) do %>
13
+ <% DoorCheck::SecondStep.offered_setups.each do |setup| %>
14
+ <%= ui_button(label: setup.label, href: door_check.public_send(setup.page), method: setup.request_method, variant: :secondary, full_width: true) %>
15
+ <% end %>
16
+ <% end %>
17
+ <% end %>
7
18
  <% end %>
8
19
  <% end %>
9
- <%= ui_data_table(items: DoorCheck::SignedInSession.where(person: person, signed_out_at: nil).order(last_used_at: :desc),
10
- columns: [ { device: "Signed in on" }, { last_used: "Last used" } ],
11
- empty_message: "No signed-in sessions.") do |table| %>
12
- <% table.actions do |signed_in_session| %>
13
- <%= ui_action_menu_item(label: "Sign out", href: "#{submit_urls[:sign_out_session]}?signed_in_session_id=#{signed_in_session.id}", method: :patch,
14
- confirm: "Sign out this session?") %>
20
+ <%= ui_section(title: "Signed-in sessions") do %>
21
+ <%= ui_panel do %>
22
+ <%= ui_data_table(items: DoorCheck::SignedInSession.where(person: person, signed_out_at: nil).order(last_used_at: :desc),
23
+ columns: [ { device: "Signed in on" }, { last_used: "Last used" } ],
24
+ empty_message: "No signed-in sessions.", phone_list: true) do |table| %>
25
+ <% table.actions do |signed_in_session| %>
26
+ <%= ui_action_menu_item(label: "Sign out", href: "#{submit_urls[:sign_out_session]}?signed_in_session_id=#{signed_in_session.id}", method: :patch,
27
+ confirm: "Sign out this session?") %>
28
+ <% end %>
29
+ <% end %>
15
30
  <% end %>
16
31
  <% end %>
17
32
  <% if DoorCheck.configuration.offered_methods.include?(:trusted_device) %>
18
- <%= ui_data_table(items: DoorCheck::TrustedDevice.where(person: person).where("expires_at > ?", Time.current),
19
- columns: [ { device: "Trusted browser" }, { trusted_until: "Trusted until" } ],
20
- empty_message: "No trusted browsers.") do |table| %>
21
- <% table.actions do |trusted_device| %>
22
- <%= ui_action_menu_item(label: "Remove", href: "#{submit_urls[:remove_trusted_browser]}?trusted_device_id=#{trusted_device.id}", method: :patch,
23
- confirm: "Remove this trusted browser?") %>
33
+ <%= ui_section(title: "Trusted browsers") do %>
34
+ <%= ui_panel do %>
35
+ <%= ui_data_table(items: DoorCheck::TrustedDevice.where(person: person).where("expires_at > ?", Time.current),
36
+ columns: [ { device: "Trusted browser" }, { trusted_until: "Trusted until" } ],
37
+ empty_message: "No trusted browsers.", phone_list: true) do |table| %>
38
+ <% table.actions do |trusted_device| %>
39
+ <%= ui_action_menu_item(label: "Remove", href: "#{submit_urls[:remove_trusted_browser]}?trusted_device_id=#{trusted_device.id}", method: :patch,
40
+ confirm: "Remove this trusted browser?") %>
41
+ <% end %>
42
+ <% end %>
24
43
  <% end %>
25
44
  <% end %>
26
45
  <% end %>
27
- <% if DoorCheck.configuration.offered_methods.include?(:authenticator_app) %>
28
- <%= ui_button(label: "Set up an authenticator app", href: door_check.new_authenticator_path, variant: :secondary) %>
29
- <% end %>
30
- <% if DoorCheck.configuration.offered_methods.include?(:passkey) %>
31
- <%= ui_button(label: "Add a passkey or hardware key", href: door_check.new_passkey_path, variant: :secondary) %>
32
- <% end %>
33
- <% if DoorCheck.configuration.offered_methods.include?(:backup_codes) %>
34
- <%= ui_button(label: "Make new backup codes", href: door_check.backup_codes_path, method: :post, variant: :secondary) %>
35
- <% end %>
36
- <%= ui_form(action: submit_urls[:own_second_step], method: :patch) do %>
37
- <%= ui_form_field(attribute: "second_step_required", label: "Ask me for a second step every time I sign in",
38
- type: :checkbox, value: (DoorCheck::PersonRule.for(person).second_step_required ? "1" : "0")) %>
39
- <%= ui_button(label: "Save") %>
46
+ <%= ui_section(title: "Second step") do %>
47
+ <%= ui_panel do %>
48
+ <%= ui_grid(gap: :md) do %>
49
+ <% if local_assigns[:account] && DoorCheck::AccountRule.for(local_assigns[:account]).second_step_required %>
50
+ <%= ui_alert(message: "Your account asks everyone for a second step.") %>
51
+ <% end %>
52
+ <% if !DoorCheck.configuration.people_choose_second_step %>
53
+ <%= ui_alert(message: "This app decides when you are asked for a second step.") %>
54
+ <% elsif DoorCheck::PersonRule.for(person).may_choose_in?(local_assigns[:account]) %>
55
+ <%= ui_form(action: submit_urls[:own_second_step], method: :patch) do %>
56
+ <%= ui_form_field(attribute: "second_step_required", label: "Ask me for a second step every time I sign in",
57
+ type: :checkbox, value: (DoorCheck::PersonRule.for(person).second_step_required ? "1" : "0")) %>
58
+ <%= ui_button(label: "Save") %>
59
+ <% end %>
60
+ <% else %>
61
+ <%= ui_alert(message: "Your account decides whether you are asked for a second step.") %>
62
+ <% end %>
63
+ <% end %>
64
+ <% end %>
40
65
  <% end %>
@@ -1,10 +1,24 @@
1
1
  <% rule = DoorCheck::AccountRule.for(account) %>
2
- <%= ui_form(action: submit_url, method: :patch) do %>
3
- <% DoorCheck.configuration.offered_methods.each do |method_name| %>
4
- <%= ui_checkbox_row(name: "accepted_methods[]", value: method_name, label: method_name.to_s.humanize, checked: rule.accepts?(method_name)) %>
2
+ <%= ui_panel do %>
3
+ <%= ui_form(action: submit_url, method: :patch) do %>
4
+ <%= ui_section(title: "Accepted methods", subtitle: "Members enter this account only after using one of the ticked methods.", spacing: :sm) do %>
5
+ <% DoorCheck.configuration.offered_methods.each do |method_name| %>
6
+ <%= ui_checkbox_row(name: "accepted_methods[]", value: method_name, label: method_name.to_s.humanize, checked: rule.accepts?(method_name)) %>
7
+ <% end %>
8
+ <% end %>
9
+ <%= ui_section(title: "Second step", subtitle: "When ticked, members who signed in with only a password are asked for a second step before they enter.", spacing: :sm) do %>
10
+ <%= ui_form_field(attribute: "second_step_required", label: "Require a second step to enter this account",
11
+ type: :checkbox, value: (rule.second_step_required ? "1" : "0")) %>
12
+ <% end %>
13
+ <% people_choose = DoorCheck.configuration.people_choose_second_step %>
14
+ <%= ui_section(title: "Members", subtitle: (people_choose ? "When ticked, a member who asks for a second step every time they sign in is asked for it in this account." : nil), spacing: :sm) do %>
15
+ <% if people_choose %>
16
+ <%= ui_form_field(attribute: "members_choose_second_step", label: "Members choose their own second step",
17
+ type: :checkbox, value: (rule.members_choose_second_step ? "1" : "0")) %>
18
+ <% else %>
19
+ <%= ui_alert(message: "This app decides when members are asked for a second step.") %>
20
+ <% end %>
21
+ <% end %>
22
+ <%= ui_button(label: "Save") %>
5
23
  <% end %>
6
- <%= ui_form_field(attribute: "second_step_required", label: "Require a second step to enter this account",
7
- type: :checkbox, value: (rule.second_step_required ? "1" : "0"),
8
- hint: "Members who signed in with only a password are asked for their authenticator code when they enter.") %>
9
- <%= ui_button(label: "Save") %>
10
24
  <% end %>
@@ -0,0 +1,5 @@
1
+ class AddMembersChooseSecondStepToDoorCheckAccountRules < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :door_check_account_rules, :members_choose_second_step, :boolean, default: true, null: false
4
+ end
5
+ end
@@ -7,12 +7,14 @@ module DoorCheck
7
7
  attr_reader :offered_methods
8
8
  attr_writer :authenticator_issuer
9
9
  attr_accessor :person_method, :account_method, :parent_controller, :parent_mailer, :existing_authenticator, :person_accounts,
10
- :find_person_by_email, :sign_in_person, :sign_out_person, :passkey_domain, :locate_ip, :stylesheet, :head_partial
10
+ :find_person_by_email, :sign_in_person, :sign_out_person, :passkey_domain, :locate_ip, :stylesheet, :head_partial,
11
+ :people_choose_second_step
11
12
 
12
13
  def initialize
13
14
  @parent_controller = "::ApplicationController"
14
15
  @parent_mailer = "::ApplicationMailer"
15
16
  @stylesheet = "application"
17
+ @people_choose_second_step = true
16
18
  end
17
19
 
18
20
  def offered_methods=(names)
@@ -1,3 +1,3 @@
1
1
  module DoorCheck
2
- VERSION = "0.3.2"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -34,7 +34,7 @@ Method names:
34
34
  Rule and session actions. Each is built with `person:`, `account:` and
35
35
  `values:`, all three required, and `call` returns a `DoorCheck::Result`:
36
36
 
37
- - `DoorCheck::RequireSecondStep` — saves the account's rule: the methods it accepts from `values[:accepted_methods]`, and whether it requires a second step from `values[:second_step_required]`.
37
+ - `DoorCheck::RequireSecondStep` — saves the account's rule: the methods it accepts from `values[:accepted_methods]`, whether it requires a second step from `values[:second_step_required]`, and whether its members choose their own second step from `values[:members_choose_second_step]`.
38
38
  - `DoorCheck::RequireOwnSecondStep` — saves the person's own rule: whether they require a second step, from `values[:second_step_required]`.
39
39
  - `DoorCheck::RemoveMethod` — removes the person's saved method whose id is `values[:credential_id]`.
40
40
  - `DoorCheck::SignOutSession` — signs out the person's signed-in session whose id is `values[:signed_in_session_id]`.
@@ -133,7 +133,8 @@ class SettingsController < ApplicationController
133
133
 
134
134
  def sign_in_rules
135
135
  finish DoorCheck::RequireSecondStep.new(person: current_user, account: door_check_account,
136
- values: params.permit(:second_step_required, accepted_methods: [])).call
136
+ values: params.permit(:second_step_required, :members_choose_second_step,
137
+ accepted_methods: [])).call
137
138
  end
138
139
 
139
140
  private
@@ -151,11 +152,41 @@ end
151
152
  `current_user` stands for the host's person method. Use the same method the
152
153
  install local gave door_check as the person method.
153
154
 
155
+ The person section shows titled groups, in this order: Sign-in methods,
156
+ Signed-in sessions, Trusted browsers and Second step. It leaves out the Trusted
157
+ browsers group when the app does not offer `trusted_device`, so the trusted
158
+ browser route is only reached when `trusted_device` is offered. Ask the
159
+ developer whether to write that route when it is not offered.
160
+
161
+ The account section is one panel holding one form, with titled groups in this
162
+ order: Accepted methods, Second step and Members, then a single Save button.
163
+ That one Save sends every group's values together to the `sign_in_rules`
164
+ route.
165
+
154
166
  What each section sends, and what the action does with it:
155
167
 
168
+ - The Second step group shows the alert "Your account asks everyone for a
169
+ second step." when it was rendered with an account whose rule requires a
170
+ second step. It shows this as well as the checkbox or the alerts below.
171
+ - When the app is set so people do not choose their own second step, the
172
+ person section shows the alert "This app decides when you are asked for a
173
+ second step." in place of the second-step checkbox, whatever the account
174
+ allows, and sends nothing to the own second-step route.
175
+ - Otherwise the person section shows its second-step checkbox only while the
176
+ account it was rendered with lets members choose, or when it was rendered
177
+ with no account. When the account does not let members choose, it shows the
178
+ alert "Your account decides whether you are asked for a second step." in
179
+ place of the checkbox, and sends nothing to the own second-step route.
156
180
  - The person section's second-step form sends `second_step_required`.
157
181
  `RequireOwnSecondStep` saves the rule as on when the value is `"1"` and off
158
182
  for any other value.
183
+ - `RequireOwnSecondStep` refuses the save, keeps the person's earlier choice,
184
+ and returns a result whose `ok?` is false when the person may not choose
185
+ with the `account` it was given:
186
+ - When the app is set so people do not choose their own second step, the
187
+ `message` is `"This app decides when you are asked for a second step."`.
188
+ - Otherwise, when `account` is not nil and does not let members choose, the
189
+ `message` is `"Your account decides whether you are asked for a second step."`.
159
190
  - The person section's Remove on a saved method sends `credential_id`.
160
191
  `RemoveMethod` removes that method only when it belongs to `person`.
161
192
  - The person section's Sign out on a session sends `signed_in_session_id`.
@@ -166,11 +197,28 @@ What each section sends, and what the action does with it:
166
197
  `RemoveTrustedBrowser` forgets that browser only when it belongs to `person`,
167
198
  and the browser is asked for a second step again.
168
199
  - The account section sends `accepted_methods[]`, one value per ticked method,
169
- and `second_step_required`. `RequireSecondStep` refuses the save when a
170
- ticked method is not one the app offers, and returns a result whose `ok?` is
171
- false and whose `message` is `"Pick only methods this app offers."`.
172
- Otherwise it saves both values to the rule of the `account` it was given, with
173
- the second step on only when `second_step_required` is `"1"`.
200
+ `second_step_required` and `members_choose_second_step`. `RequireSecondStep`
201
+ refuses the save when a ticked method is not one the app offers, and returns
202
+ a result whose `ok?` is false and whose `message` is
203
+ `"Pick only methods this app offers."`. Otherwise it saves the values to the
204
+ rule of the `account` it was given, with the second step on only when
205
+ `second_step_required` is `"1"` and members choosing only when
206
+ `members_choose_second_step` is `"1"`.
207
+ - When the app is set so people do not choose their own second step, the
208
+ account section shows the alert "This app decides when members are asked for
209
+ a second step." in place of the members checkbox, and sends no
210
+ `members_choose_second_step`.
211
+ - When the app is set so people do not choose their own second step and
212
+ `values` has a `members_choose_second_step` key, whatever its value,
213
+ `RequireSecondStep` refuses the save, keeps the account's earlier rule, and
214
+ returns a result whose `ok?` is false and whose `message` is
215
+ `"This app decides when members are asked for a second step."`. A save that
216
+ ticks a method the app does not offer is refused with the offered-methods
217
+ message first.
218
+ - Permit `members_choose_second_step` in the `sign_in_rules` action. When
219
+ `values` has no `members_choose_second_step` key, `RequireSecondStep` leaves
220
+ the account's setting as it was, so an action that does not permit it never
221
+ changes it.
174
222
 
175
223
  Ask the developer who may save the account's rules. door_check does not check
176
224
  it, so the `sign_in_rules` action must refuse anyone the host does not allow,
@@ -202,22 +250,26 @@ before building `RequireSecondStep`.
202
250
  3. The sign-in email always carries both a code and a link. The link returns
203
251
  not found unless `email_link` is offered, so ask the developer whether to
204
252
  offer `email_link` whenever `email_code` is offered.
205
- 4. Put the sign-in links on the host's sign-in page, for a person who is not
253
+ 4. The person section's Sign-in methods group already shows buttons to set up
254
+ an authenticator app, add a passkey and make new backup codes, each only
255
+ when its method is offered. Add those links elsewhere only when the
256
+ developer asks for them outside the settings page.
257
+ 5. Put the sign-in links on the host's sign-in page, for a person who is not
206
258
  signed in. The approvals, authenticator, passkey and backup codes links are
207
259
  for a signed-in person only.
208
- 5. Link to `backup_codes_path` with a form that sends a `POST`, such as
260
+ 6. Link to `backup_codes_path` with a form that sends a `POST`, such as
209
261
  `button_to`, never a plain link.
210
- 6. When the person already has a second step set up, the authenticator, passkey
262
+ 7. When the person already has a second step set up, the authenticator, passkey
211
263
  and backup codes pages first ask for it if this session has not passed one,
212
264
  then send the person back.
213
- 7. The authenticator page keeps showing the same secret key in this browser
265
+ 8. The authenticator page keeps showing the same secret key in this browser
214
266
  session until setup succeeds, so reloading it or entering a wrong code does
215
267
  not change the key the person already scanned. A wrong code shows an error on
216
268
  the same page and saves nothing.
217
- 8. When the browser session no longer holds the key, such as after the person
269
+ 9. When the browser session no longer holds the key, such as after the person
218
270
  signed out and back in, saving a code sends the person back to the
219
271
  authenticator page with a new key and a warning to scan it again.
220
- 9. A right code saves the authenticator app, counts as this session's second
272
+ 10. A right code saves the authenticator app, counts as this session's second
221
273
  step, and sends the person to the page they were on before door_check asked
222
274
  for a second step, or to `/` when there was none. The app lists the account
223
275
  under the person's email when the person responds to `email`.
@@ -227,15 +279,34 @@ before building `RequireSecondStep`.
227
279
  - Never edit door_check's records directly. Change rules, sessions, saved methods
228
280
  and trusted browsers through the five actions above.
229
281
  - Pass the five actions the person from the host's person method and the
230
- account from `door_check_account`. `RequireOwnSecondStep`, `RemoveMethod`,
282
+ account from `door_check_account`. `RequireOwnSecondStep` decides from that
283
+ account whether the person may choose, so passing another account gives a
284
+ different answer than the settings section showed. `RemoveMethod`,
231
285
  `SignOutSession` and `RemoveTrustedBrowser` take `account:` but do not use it.
232
- - `RequireOwnSecondStep`, `RemoveMethod`, `SignOutSession` and
233
- `RemoveTrustedBrowser` always return a result whose `ok?` is true. An id
234
- that does not belong to the person changes nothing and still returns true.
286
+ - Show `result.message` whenever `ok?` is false, since `RequireSecondStep` and
287
+ `RequireOwnSecondStep` can both refuse a save.
288
+ - `RemoveMethod`, `SignOutSession` and `RemoveTrustedBrowser` always return a
289
+ result whose `ok?` is true. An id that does not belong to the person changes
290
+ nothing and still returns true.
235
291
  - When no `accepted_methods[]` box is ticked, the form sends none and the
236
292
  account's rule is saved as accepting every offered method.
237
- - A person's rule and an account's rule both apply. A person who requires a
238
- second step is asked for one whatever their account's rule says.
293
+ - An account lets its members choose their own second step until its rule is
294
+ saved with `members_choose_second_step` off.
295
+ - While the app lets people choose and the current account lets members
296
+ choose, a person's rule and the account's rule both apply, so a person who requires a second step is asked
297
+ for one whatever the account requires.
298
+ - While the current account does not let members choose, the person's own rule
299
+ is not in force there, and only the account's rule decides whether a second
300
+ step is asked for. The person's saved choice is kept, and it applies again
301
+ once the account lets members choose.
302
+ - When `door_check_account` is nil, the person's own rule is in force unless
303
+ the app is set so people do not choose their own second step.
304
+ - While the app is set so people do not choose their own second step, no
305
+ person's own rule is in force in any account or with no account, and only
306
+ the account's rule decides whether a second step is asked for. Each person's
307
+ saved choice is kept, and it applies again once the app lets people choose.
308
+ Read the host's door_check initializer, or ask the developer, to learn which
309
+ way the app is set.
239
310
  - Out of scope: adding the gem, its migrations, its routes, its settings, the
240
311
  host controller's tracking and second-step check, and placing the settings
241
312
  sections. Those belong to the install local.
@@ -62,12 +62,56 @@ door_check declares nothing for this local to document.
62
62
  - **Offered methods** — the subset of known methods this app turns on. Naming a
63
63
  method door_check does not know is an error at boot. A page for a method the
64
64
  app does not offer returns not found.
65
- - **Account rule** — whether an account requires a second step, and which of the
66
- offered methods it accepts. An account can only narrow what the app offers.
67
- - **Person rule** — whether a person requires a second step on their own login,
68
- whatever their account says.
65
+ - **People choosing their own second step** — an app-wide switch, on by
66
+ default. When the app turns it off, no person rule is in force anywhere, and
67
+ only account rules decide when a second step is asked for.
68
+ - **Account rule** — whether an account requires a second step, which of the
69
+ offered methods it accepts, and whether its members choose their own second
70
+ step. An account can only narrow what the app offers. Members choose by
71
+ default, and only while the app lets people choose.
72
+ - **Refused account rule save** — saving an account rule that accepts a method
73
+ the app does not offer is refused with "Pick only methods this app offers."
74
+ Saving one that sets whether members choose, while the app does not let people
75
+ choose, is refused with "This app decides when members are asked for a second
76
+ step." A refused save changes nothing, and the account keeps its earlier rule.
77
+ - **Person rule** — whether a person requires a second step on their own login.
78
+ It is in force only while the app lets people choose, and then with no
79
+ account or in an account that lets members choose. Elsewhere only the
80
+ account's rule decides, and the person's saved choice is kept and applies
81
+ again once both the app and the account let them choose.
82
+ - **Refused person rule save** — saving a person rule where the person may not
83
+ choose is refused. The message is "This app decides when you are asked for a
84
+ second step." when the app does not let people choose, and "Your account
85
+ decides whether you are asked for a second step." when the person's account
86
+ does not let members choose. A refused save changes nothing, and the person
87
+ keeps their earlier choice.
88
+ - **Personal settings section** — the section where a person sees their saved
89
+ methods, signed-in sessions and trusted browsers, and sets their person rule.
90
+ It shows four titled groups, each in its own panel: Sign-in methods, with the
91
+ buttons to add one; Signed-in sessions; Trusted browsers, left out when the
92
+ app does not offer trusted browsers; and Second step. On a phone each table
93
+ shows as a list and the add buttons stack at full width.
94
+ - **Second step group** — the part of the personal settings section that holds
95
+ the person rule's checkbox. When the person's current account requires a
96
+ second step, it shows the note "Your account asks everyone for a second
97
+ step." above the rest. When the app does not let people choose, it shows the
98
+ note "This app decides when you are asked for a second step." in place of the
99
+ checkbox. When the person's current account does not let members choose, it
100
+ shows the note "Your account decides whether you are asked for a second
101
+ step." instead.
102
+ - **Account settings section** — the section where an account's rule is set,
103
+ in one panel with one Save button. It shows three titled groups, each with a
104
+ line saying what ticking it does. Accepted methods lists every offered method,
105
+ and members enter the account only after using one of the ticked methods.
106
+ Second step holds the checkbox that makes members who signed in with only a
107
+ password take a second step before they enter. Members holds the checkbox that
108
+ lets a member's own second-step choice apply in this account. When the app
109
+ does not let people choose, the Members group shows the note "This app
110
+ decides when members are asked for a second step." in place of its checkbox
111
+ and line.
69
112
  - **Proof** — a method a session has passed. A person may enter a page once the
70
- session's proofs meet both their person rule and their current account's rule.
113
+ session's proofs meet their current account's rule and, where it is in force,
114
+ their person rule.
71
115
  Otherwise they are sent to the second step page and returned afterwards.
72
116
  - **Signed-in session** — one sign-in on one device, with its device, IP address
73
117
  and last use. Ending one signs that device out on its next request.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: door_check-install
3
- description: Use to hook door_check into a project — adding the gem, copying its migrations, mounting its routes, configuring the offered methods and the host's person, account, sign-in and sign-out hooks, the parent controller and mailer, the passkey domain, the IP lookup, the stylesheet and head partial, the name shown in authenticator apps, adding the sign-in tracking and second-step check to the host's controller, and placing the two settings sections.
3
+ description: Use to hook door_check into a project — adding the gem, copying its migrations, mounting its routes, configuring the offered methods and the host's person, account, sign-in and sign-out hooks, the parent controller and mailer, the passkey domain, the IP lookup, the stylesheet and head partial, the name shown in authenticator apps, whether people may choose their own second step, adding the sign-in tracking and second-step check to the host's controller, and placing the two settings sections.
4
4
  tools: Bash, Read, Edit
5
5
  scope: sign-in — the sign-in methods and second steps an app offers, each account narrows, and each person chooses from
6
6
  ---
@@ -36,11 +36,12 @@ trusted browsers.
36
36
  - `config.stylesheet` — the stylesheet door_check's pages link.
37
37
  - `config.head_partial` — a host partial rendered at the end of the head of door_check's pages.
38
38
  - `config.authenticator_issuer` — the name an authenticator app shows above the account it adds during setup.
39
+ - `config.people_choose_second_step` — whether people may require a second step on their own login, true or false.
39
40
  - `include DoorCheck::Tracking` — adds sign-in recording and the reads of proofs and the current account to the host's controller.
40
41
  - `include DoorCheck::EntryCheck` — adds the second-step check to the host's controller.
41
42
  - `before_action :door_check_check_entry` — runs that check on every host request.
42
43
  - `door_check/settings/sign_in_methods` — the partial a person uses to see and remove their methods, sessions and trusted browsers, set up new ones, and require a second step on their own login.
43
- - `door_check/settings/sign_in_rules` — the partial an account admin uses to choose which offered methods the account accepts and whether it requires a second step.
44
+ - `door_check/settings/sign_in_rules` — the partial an account admin uses to choose which offered methods the account accepts, whether it requires a second step, and whether its members' own second-step choice applies inside it.
44
45
 
45
46
  ## How to use it
46
47
 
@@ -139,6 +140,17 @@ trusted browsers.
139
140
  application's module, such as `"Shop"` for `Shop::Application`. Set it when
140
141
  people should see another name in their authenticator app. Changing it does
141
142
  not rename accounts people already added.
143
+ - `config.people_choose_second_step = false` — defaults to true. Set it to
144
+ false when only the app and its accounts' rules decide who is asked for a
145
+ second step. While it is false, no person's saved choice is applied in any
146
+ account, the personal section shows "This app decides when you are asked
147
+ for a second step." in place of its checkbox, and the account section
148
+ shows "This app decides when members are asked for a second step." in
149
+ place of its members checkbox. While it is false, door_check also refuses
150
+ a save of a person's own second-step choice with the first message, and a
151
+ save of the account section that carries the members choice with the
152
+ second. A refused save leaves the earlier value in place. Saved choices
153
+ are kept, and apply again if it is set back to true.
142
154
 
143
155
  8. Add the tracking and the second-step check to the host's
144
156
  `app/controllers/application_controller.rb`, or to the controller named in
@@ -164,6 +176,7 @@ trusted browsers.
164
176
  ```erb
165
177
  <%= render "door_check/settings/sign_in_methods",
166
178
  person: current_user,
179
+ account: current_account,
167
180
  submit_urls: {
168
181
  own_second_step: own_second_step_settings_path,
169
182
  remove: remove_sign_in_method_settings_path,
@@ -181,7 +194,54 @@ trusted browsers.
181
194
  - `remove` receives `credential_id`.
182
195
  - `sign_out_session` receives `signed_in_session_id`.
183
196
  - `remove_trusted_browser` receives `trusted_device_id`.
184
- - The account section's URL receives `accepted_methods[]` and `second_step_required`.
197
+ - The account section's URL receives `accepted_methods[]`,
198
+ `second_step_required` and `members_choose_second_step`. While
199
+ `people_choose_second_step` is false it does not send
200
+ `members_choose_second_step`.
201
+ - The host action behind the account section's URL must pass
202
+ `members_choose_second_step` on only when the request carries it.
203
+ door_check keeps the account's earlier value when the parameter is
204
+ missing, and refuses the save when it is present while
205
+ `people_choose_second_step` is false.
206
+
207
+ The personal section renders its own titled groups, so place it on the page
208
+ without a heading or panel around it:
209
+ - **Sign-in methods** — the person's saved methods, with a button to set up
210
+ an authenticator app, a passkey or new backup codes for each of those
211
+ that `offered_methods` includes.
212
+ - **Signed-in sessions** — the person's sessions that are still signed in.
213
+ - **Trusted browsers** — shown only when `trusted_device` is offered.
214
+ - **Second step** — the person's own second-step checkbox, or a note in its
215
+ place.
216
+
217
+ On a phone-width screen each table shows as a list and the setup buttons
218
+ stack at full width.
219
+
220
+ The account section renders its own panel, holding three titled groups and
221
+ one Save button, so place it on the page without a heading or panel around
222
+ it either:
223
+ - **Accepted methods** — a checkbox for each method in `offered_methods`.
224
+ Members enter the account only after using one of the ticked methods.
225
+ - **Second step** — the checkbox that asks members who signed in with only
226
+ a password for a second step before they enter.
227
+ - **Members** — the checkbox that lets a member's own second-step choice
228
+ apply in this account, or, while `people_choose_second_step` is false,
229
+ the note "This app decides when members are asked for a second step." in
230
+ its place.
231
+
232
+ Every account starts out letting its members choose their own second step.
233
+ While an account does not, a person's own choice is not applied inside that
234
+ account, and only the account's own rule decides.
235
+
236
+ Pass the person's current account as `account:` to the personal section.
237
+ When that account requires a second step, the Second step group shows
238
+ "Your account asks everyone for a second step." above the checkbox or note.
239
+ When that account does not let members choose, the group shows the note
240
+ "Your account decides whether you are asked for a second step." in place of
241
+ the checkbox. A save of the person's own choice in that account is refused
242
+ with the same note, and the person's earlier choice is kept. Leave
243
+ `account:` out only when the host has no accounts, since without it the
244
+ checkbox always shows and no account note appears.
185
245
 
186
246
  The controller actions behind those routes are the develop local's to
187
247
  write. Ask the developer which settings page each section goes on, and who
@@ -20,6 +20,7 @@ install:
20
20
  - config.stylesheet
21
21
  - config.head_partial
22
22
  - config.authenticator_issuer
23
+ - config.people_choose_second_step
23
24
  - include DoorCheck::Tracking
24
25
  - include DoorCheck::EntryCheck
25
26
  - before_action :door_check_check_entry
@@ -68,6 +69,7 @@ sources:
68
69
  - app/models/door_check/challenge.rb
69
70
  - app/models/door_check/require_second_step.rb
70
71
  - app/models/door_check/require_own_second_step.rb
72
+ - app/models/door_check/person_rule.rb
71
73
  - app/models/door_check/remove_method.rb
72
74
  - app/models/door_check/sign_out_session.rb
73
75
  - app/models/door_check/remove_trusted_browser.rb
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: door_check
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Schneider
@@ -174,6 +174,7 @@ files:
174
174
  - db/migrate/20261111162243_create_door_check_trusted_devices.rb
175
175
  - db/migrate/20261111170231_add_device_to_door_check_signed_in_sessions.rb
176
176
  - db/migrate/20261111170432_add_approval_to_door_check_challenges.rb
177
+ - db/migrate/20261113191736_add_members_choose_second_step_to_door_check_account_rules.rb
177
178
  - lib/door_check.rb
178
179
  - lib/door_check/configuration.rb
179
180
  - lib/door_check/engine.rb