practical 0.1.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 +7 -0
- data/README.md +37 -0
- data/Rakefile +10 -0
- data/app/components/practical/views/base_component.rb +6 -0
- data/app/components/practical/views/button_component.rb +27 -0
- data/app/components/practical/views/datatable/filter_applied.rb +25 -0
- data/app/components/practical/views/datatable/filter_section_component.html.erb +9 -0
- data/app/components/practical/views/datatable/filter_section_component.rb +19 -0
- data/app/components/practical/views/datatable/sort_link_component.rb +48 -0
- data/app/components/practical/views/datatable.rb +36 -0
- data/app/components/practical/views/flash_messages_component.rb +65 -0
- data/app/components/practical/views/form/error_list_component.rb +15 -0
- data/app/components/practical/views/form/error_list_item_component.rb +20 -0
- data/app/components/practical/views/form/error_list_item_template_component.rb +9 -0
- data/app/components/practical/views/form/fallback_errors_section_component.html.erb +7 -0
- data/app/components/practical/views/form/fallback_errors_section_component.rb +21 -0
- data/app/components/practical/views/form/field_errors_component.rb +28 -0
- data/app/components/practical/views/form/field_title_component.rb +23 -0
- data/app/components/practical/views/form/fieldset_title_component.rb +20 -0
- data/app/components/practical/views/form/input_component.html.erb +7 -0
- data/app/components/practical/views/form/input_component.rb +22 -0
- data/app/components/practical/views/form/option_label_component.rb +21 -0
- data/app/components/practical/views/form/practical_editor_component.rb +26 -0
- data/app/components/practical/views/form/required_radio_collection_wrapper_component.rb +23 -0
- data/app/components/practical/views/form_wrapper.rb +21 -0
- data/app/components/practical/views/icon_component.rb +36 -0
- data/app/components/practical/views/icon_for_file_extension_component.rb +53 -0
- data/app/components/practical/views/modal_dialog_component.html.erb +10 -0
- data/app/components/practical/views/modal_dialog_component.rb +16 -0
- data/app/components/practical/views/navigation/breadcrumb_item_component.rb +20 -0
- data/app/components/practical/views/navigation/breadcrumbs_component.html.erb +31 -0
- data/app/components/practical/views/navigation/breadcrumbs_component.rb +41 -0
- data/app/components/practical/views/navigation/navigation_link_component.rb +39 -0
- data/app/components/practical/views/navigation/pagination/goto_form_component.html.erb +31 -0
- data/app/components/practical/views/navigation/pagination/goto_form_component.rb +34 -0
- data/app/components/practical/views/navigation/pagination_component.html.erb +11 -0
- data/app/components/practical/views/navigation/pagination_component.rb +98 -0
- data/app/components/practical/views/open_dialog_button_component.rb +16 -0
- data/app/components/practical/views/page_component.html.erb +53 -0
- data/app/components/practical/views/page_component.rb +12 -0
- data/app/components/practical/views/relative_time_component.rb +13 -0
- data/app/components/practical/views/tiptap_document_component.rb +311 -0
- data/app/components/practical/views/toast_component.html.erb +26 -0
- data/app/components/practical/views/toast_component.rb +19 -0
- data/app/controllers/concerns/practical/auth/passkeys/emergency_registrations.rb +57 -0
- data/app/controllers/concerns/practical/auth/passkeys/web_authn_debug_context.rb +13 -0
- data/app/controllers/concerns/practical/views/flash_helpers.rb +37 -0
- data/app/controllers/concerns/practical/views/json_redirection.rb +7 -0
- data/app/lib/practical/defaults/shrine.rb +48 -0
- data/app/lib/practical/test/helpers/administrator/test_helpers.rb +7 -0
- data/app/lib/practical/test/helpers/extra_assertions.rb +7 -0
- data/app/lib/practical/test/helpers/flash_assertions.rb +8 -0
- data/app/lib/practical/test/helpers/integration/assertions.rb +23 -0
- data/app/lib/practical/test/helpers/passkey/system/base.rb +52 -0
- data/app/lib/practical/test/helpers/passkey/system/rack_test.rb +45 -0
- data/app/lib/practical/test/helpers/passkey/system/selenium.rb +107 -0
- data/app/lib/practical/test/helpers/passkey/test_helper.rb +128 -0
- data/app/lib/practical/test/helpers/postmark.rb +11 -0
- data/app/lib/practical/test/helpers/relation_builder_assertions.rb +18 -0
- data/app/lib/practical/test/helpers/setup/debug.rb +8 -0
- data/app/lib/practical/test/helpers/setup/faker_seed_pinning.rb +8 -0
- data/app/lib/practical/test/helpers/setup/simplecov.rb +17 -0
- data/app/lib/practical/test/helpers/shrine/test_data.rb +101 -0
- data/app/lib/practical/test/helpers/spy_assertions.rb +7 -0
- data/app/lib/practical/test/helpers/system/assertions.rb +33 -0
- data/app/lib/practical/test/helpers/system/capybara_prep.rb +10 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +372 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/self_service.rb +66 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +119 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_destroy.rb +13 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/no_self_signup.rb +22 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +134 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +221 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +220 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/base.rb +108 -0
- data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +82 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/base.rb +89 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/emergency_registration/use_for_and_notify.rb +48 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/passkey.rb +101 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys.rb +57 -0
- data/app/lib/practical/test/shared/auth/passkeys/policies/passkey.rb +18 -0
- data/app/lib/practical/test/shared/auth/passkeys/services/send_emergency_registration.rb +41 -0
- data/app/lib/practical/test/shared/models/normalized_email.rb +23 -0
- data/app/lib/practical/test/shared/models/user.rb +27 -0
- data/app/lib/practical/test/shared/models/utility/ip_address.rb +42 -0
- data/app/lib/practical/test/shared/models/utility/user_agent.rb +43 -0
- data/app/lib/practical/views/button/styling.rb +23 -0
- data/app/lib/practical/views/error_handling.rb +33 -0
- data/app/lib/practical/views/form_builders/base.rb +152 -0
- data/app/lib/practical/views/icon_set.rb +156 -0
- data/app/lib/practical/views/web_awesome/style_utility/appearance_variant.rb +19 -0
- data/app/lib/practical/views/web_awesome/style_utility/base.rb +21 -0
- data/app/lib/practical/views/web_awesome/style_utility/color_variant.rb +17 -0
- data/app/lib/practical/views/web_awesome/style_utility/size.rb +31 -0
- data/config/locales/auth.en.yml +38 -0
- data/config/locales/devise.passkeys.en.yml +18 -0
- data/config/locales/practical_framework.en.yml +9 -0
- data/config/routes.rb +4 -0
- data/db/seeds/setup.rb +13 -0
- data/db/seeds/users/default.rb +34 -0
- data/lib/generators/practical/test/helper/USAGE +8 -0
- data/lib/generators/practical/test/helper/helper_generator.rb +9 -0
- data/lib/generators/practical/test/helper/templates/helper.rb.tt +4 -0
- data/lib/generators/practical/test/shared_test/USAGE +9 -0
- data/lib/generators/practical/test/shared_test/shared_test_generator.rb +7 -0
- data/lib/generators/practical/test/shared_test/templates/shared_test.rb.tt +9 -0
- data/lib/generators/practical/views/component/USAGE +9 -0
- data/lib/generators/practical/views/component/component_generator.rb +20 -0
- data/lib/practical/framework/engine.rb +35 -0
- data/lib/practical/helpers/form_with_helper.rb +10 -0
- data/lib/practical/helpers/icon_helper.rb +18 -0
- data/lib/practical/helpers/text_helper.rb +20 -0
- data/lib/practical/helpers/translation_helper.rb +25 -0
- data/lib/practical/version.rb +5 -0
- data/lib/practical/views/element_helper.rb +48 -0
- data/lib/practical.rb +21 -0
- data/lib/tasks/practical/coverage.rake +19 -0
- data/lib/tasks/practical/framework_tasks.rake +6 -0
- metadata +303 -0
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::SelfDestroy
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "destroy deletes the given resource when reauthenticated" do
|
8
|
+
sign_in_as_resource
|
9
|
+
|
10
|
+
client = webauthn_client
|
11
|
+
create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
|
12
|
+
|
13
|
+
new_reauthentication_challenge_action
|
14
|
+
assert_response :ok
|
15
|
+
assert_reauthentication_token_challenge
|
16
|
+
|
17
|
+
challenge = response.parsed_body["challenge"]
|
18
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
19
|
+
|
20
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
21
|
+
assert_response :ok
|
22
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
23
|
+
assert_nil expected_stored_reauthentication_challenge
|
24
|
+
|
25
|
+
reauthentication_token = response.parsed_body["reauthentication_token"]
|
26
|
+
params = params_for_destruction(reauthentication_token: reauthentication_token)
|
27
|
+
|
28
|
+
resource_id = resource_instance.id
|
29
|
+
|
30
|
+
assert_difference "#{resource_class}.count", -1 do
|
31
|
+
destroy_registration_action(params: params)
|
32
|
+
assert_redirected_to destroy_success_url
|
33
|
+
end
|
34
|
+
|
35
|
+
assert_nil resource_class.find_by(id: resource_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
test "destroy requires a reauthentication token" do
|
39
|
+
sign_in_as_resource
|
40
|
+
|
41
|
+
client = webauthn_client
|
42
|
+
create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
|
43
|
+
|
44
|
+
new_reauthentication_challenge_action
|
45
|
+
assert_response :ok
|
46
|
+
assert_reauthentication_token_challenge
|
47
|
+
|
48
|
+
challenge = response.parsed_body["challenge"]
|
49
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
50
|
+
|
51
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
52
|
+
assert_response :ok
|
53
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
54
|
+
assert_nil expected_stored_reauthentication_challenge
|
55
|
+
|
56
|
+
reauthentication_token = " "
|
57
|
+
params = params_for_destruction(reauthentication_token: reauthentication_token)
|
58
|
+
|
59
|
+
resource_id = resource_instance.id
|
60
|
+
|
61
|
+
assert_no_difference "#{resource_class}.count" do
|
62
|
+
destroy_registration_action(params: params)
|
63
|
+
assert_response :bad_request
|
64
|
+
assert_not_reauthenticated_message
|
65
|
+
end
|
66
|
+
|
67
|
+
assert_not_nil resource_class.find_by(id: resource_id)
|
68
|
+
end
|
69
|
+
|
70
|
+
test "destroy requires the reauthentication token to match the stored value" do
|
71
|
+
sign_in_as_resource
|
72
|
+
|
73
|
+
client = webauthn_client
|
74
|
+
create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
|
75
|
+
|
76
|
+
new_reauthentication_challenge_action
|
77
|
+
assert_response :ok
|
78
|
+
assert_reauthentication_token_challenge
|
79
|
+
|
80
|
+
challenge = response.parsed_body["challenge"]
|
81
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
82
|
+
|
83
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
84
|
+
assert_response :ok
|
85
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
86
|
+
assert_nil expected_stored_reauthentication_challenge
|
87
|
+
|
88
|
+
reauthentication_token = SecureRandom.hex
|
89
|
+
params = params_for_destruction(reauthentication_token: reauthentication_token)
|
90
|
+
|
91
|
+
resource_id = resource_instance.id
|
92
|
+
|
93
|
+
assert_no_difference "#{resource_class}.count" do
|
94
|
+
destroy_registration_action(params: params)
|
95
|
+
assert_response :bad_request
|
96
|
+
assert_not_reauthenticated_message
|
97
|
+
end
|
98
|
+
|
99
|
+
assert_not_nil resource_class.find_by(id: resource_id)
|
100
|
+
end
|
101
|
+
|
102
|
+
test "destroy ignores an attmept to delete a different resource" do
|
103
|
+
sign_in_as_resource
|
104
|
+
|
105
|
+
client = webauthn_client
|
106
|
+
create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
|
107
|
+
|
108
|
+
new_reauthentication_challenge_action
|
109
|
+
assert_response :ok
|
110
|
+
assert_reauthentication_token_challenge
|
111
|
+
|
112
|
+
challenge = response.parsed_body["challenge"]
|
113
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
114
|
+
|
115
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
116
|
+
assert_response :ok
|
117
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
118
|
+
assert_nil expected_stored_reauthentication_challenge
|
119
|
+
|
120
|
+
reauthentication_token = " "
|
121
|
+
params = params_trying_to_destroy_other_resource(reauthentication_token: reauthentication_token)
|
122
|
+
|
123
|
+
resource_id = resource_instance.id
|
124
|
+
|
125
|
+
assert_no_difference "#{resource_class}.count" do
|
126
|
+
destroy_registration_action(params: params)
|
127
|
+
assert_response :bad_request
|
128
|
+
assert_not_reauthenticated_message
|
129
|
+
end
|
130
|
+
|
131
|
+
assert_not_nil resource_class.find_by(id: resource_id)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::SelfSignup
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "new registration challenge action returns a registration challenge" do
|
8
|
+
email = Faker::Internet.email
|
9
|
+
passkey_label = SecureRandom.hex
|
10
|
+
|
11
|
+
params = params_for_registration_challenge(email: email, passkey_label: passkey_label)
|
12
|
+
new_registration_challenge_action(params: params)
|
13
|
+
assert_response :ok
|
14
|
+
webauthn_id = response.parsed_body["user"]["id"]
|
15
|
+
user_data = expected_user_data_for_challenge(email: email, webauthn_id: webauthn_id, name: email)
|
16
|
+
|
17
|
+
assert_passkey_registration_challenge(
|
18
|
+
data: response.parsed_body,
|
19
|
+
stored_challenge: expected_stored_challenge,
|
20
|
+
relying_party_data: expected_relying_party_data,
|
21
|
+
user_data: user_data,
|
22
|
+
credentials_to_exclude: []
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
test "new registration challenge action requires an email" do
|
27
|
+
email = ""
|
28
|
+
passkey_label = SecureRandom.hex
|
29
|
+
|
30
|
+
params = params_for_registration_challenge(email: email, passkey_label: passkey_label)
|
31
|
+
new_registration_challenge_action(params: params)
|
32
|
+
assert_response :bad_request
|
33
|
+
assert_email_missing_error_message
|
34
|
+
end
|
35
|
+
|
36
|
+
test "new registration challenge action requires a passkey_label" do
|
37
|
+
email = Faker::Internet.email
|
38
|
+
passkey_label = " "
|
39
|
+
|
40
|
+
params = params_for_registration_challenge(email: email, passkey_label: passkey_label)
|
41
|
+
new_registration_challenge_action(params: params)
|
42
|
+
assert_response :bad_request
|
43
|
+
assert_passkey_label_missing_error_message
|
44
|
+
end
|
45
|
+
|
46
|
+
test "new registration action renders successfully" do
|
47
|
+
new_registration_action
|
48
|
+
assert_response :ok
|
49
|
+
end
|
50
|
+
|
51
|
+
test "create registration action creates a new resource" do
|
52
|
+
email = Faker::Internet.email
|
53
|
+
passkey_label = SecureRandom.hex
|
54
|
+
|
55
|
+
params = params_for_registration_challenge(email: email, passkey_label: passkey_label)
|
56
|
+
new_registration_challenge_action(params: params)
|
57
|
+
assert_response :ok
|
58
|
+
webauthn_id = response.parsed_body["user"]["id"]
|
59
|
+
user_data = expected_user_data_for_challenge(email: email, webauthn_id: webauthn_id, name: email)
|
60
|
+
|
61
|
+
assert_passkey_registration_challenge(
|
62
|
+
data: response.parsed_body,
|
63
|
+
stored_challenge: expected_stored_challenge,
|
64
|
+
relying_party_data: expected_relying_party_data,
|
65
|
+
user_data: user_data,
|
66
|
+
credentials_to_exclude: []
|
67
|
+
)
|
68
|
+
|
69
|
+
client = webauthn_client
|
70
|
+
challenge = expected_stored_challenge
|
71
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
72
|
+
|
73
|
+
params = params_for_registration(email: email, passkey_label: passkey_label, raw_credential: raw_credential)
|
74
|
+
|
75
|
+
assert_difference "#{passkey_class}.count", +1 do
|
76
|
+
assert_difference "#{resource_class}.count", +1 do
|
77
|
+
create_resource_action(params: params)
|
78
|
+
assert_redirected_to expected_success_url
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
new_resource = resource_class.last
|
83
|
+
credential = hydrate_response_from_raw_credential(client: client, relying_party: webauthn_relying_party, raw_credential: raw_credential).credential
|
84
|
+
|
85
|
+
new_passkey = new_resource.passkeys.last
|
86
|
+
assert_equal passkey_label, new_passkey.label
|
87
|
+
assert_equal Base64.strict_encode64(credential.id), new_passkey.external_id
|
88
|
+
assert_not_nil new_passkey.public_key
|
89
|
+
assert_not_nil new_passkey.last_used_at
|
90
|
+
end
|
91
|
+
|
92
|
+
test "create registration action does not create a duplicate resource" do
|
93
|
+
email = existing_resource.email
|
94
|
+
passkey_label = SecureRandom.hex
|
95
|
+
|
96
|
+
params = params_for_registration_challenge(email: email, passkey_label: passkey_label)
|
97
|
+
new_registration_challenge_action(params: params)
|
98
|
+
assert_response :ok
|
99
|
+
webauthn_id = response.parsed_body["user"]["id"]
|
100
|
+
user_data = expected_user_data_for_challenge(email: email, webauthn_id: webauthn_id, name: email)
|
101
|
+
|
102
|
+
assert_passkey_registration_challenge(
|
103
|
+
data: response.parsed_body,
|
104
|
+
stored_challenge: expected_stored_challenge,
|
105
|
+
relying_party_data: expected_relying_party_data,
|
106
|
+
user_data: user_data,
|
107
|
+
credentials_to_exclude: []
|
108
|
+
)
|
109
|
+
|
110
|
+
client = webauthn_client
|
111
|
+
challenge = expected_stored_challenge
|
112
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
113
|
+
|
114
|
+
params = params_for_registration(email: email, passkey_label: passkey_label, raw_credential: raw_credential)
|
115
|
+
|
116
|
+
assert_no_difference "#{passkey_class}.count" do
|
117
|
+
assert_no_difference "#{resource_class}.count" do
|
118
|
+
create_resource_action(params: params)
|
119
|
+
assert_response :unprocessable_entity
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
test "create registration action requires an email" do
|
125
|
+
email = Faker::Internet.email
|
126
|
+
passkey_label = SecureRandom.hex
|
127
|
+
|
128
|
+
params = params_for_registration_challenge(email: email, passkey_label: passkey_label)
|
129
|
+
new_registration_challenge_action(params: params)
|
130
|
+
assert_response :ok
|
131
|
+
webauthn_id = response.parsed_body["user"]["id"]
|
132
|
+
user_data = expected_user_data_for_challenge(email: email, webauthn_id: webauthn_id, name: email)
|
133
|
+
|
134
|
+
assert_passkey_registration_challenge(
|
135
|
+
data: response.parsed_body,
|
136
|
+
stored_challenge: expected_stored_challenge,
|
137
|
+
relying_party_data: expected_relying_party_data,
|
138
|
+
user_data: user_data,
|
139
|
+
credentials_to_exclude: []
|
140
|
+
)
|
141
|
+
|
142
|
+
client = webauthn_client
|
143
|
+
challenge = expected_stored_challenge
|
144
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
145
|
+
|
146
|
+
params = params_for_registration(email: " ", passkey_label: passkey_label, raw_credential: raw_credential)
|
147
|
+
|
148
|
+
assert_no_difference "#{passkey_class}.count" do
|
149
|
+
assert_no_difference "#{resource_class}.count" do
|
150
|
+
create_resource_action(params: params)
|
151
|
+
assert_response :bad_request
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
test "create registration action requires a passkey_label" do
|
157
|
+
email = Faker::Internet.email
|
158
|
+
passkey_label = SecureRandom.hex
|
159
|
+
|
160
|
+
params = params_for_registration_challenge(email: email, passkey_label: passkey_label)
|
161
|
+
new_registration_challenge_action(params: params)
|
162
|
+
assert_response :ok
|
163
|
+
webauthn_id = response.parsed_body["user"]["id"]
|
164
|
+
user_data = expected_user_data_for_challenge(email: email, webauthn_id: webauthn_id, name: email)
|
165
|
+
|
166
|
+
assert_passkey_registration_challenge(
|
167
|
+
data: response.parsed_body,
|
168
|
+
stored_challenge: expected_stored_challenge,
|
169
|
+
relying_party_data: expected_relying_party_data,
|
170
|
+
user_data: user_data,
|
171
|
+
credentials_to_exclude: []
|
172
|
+
)
|
173
|
+
|
174
|
+
client = webauthn_client
|
175
|
+
challenge = expected_stored_challenge
|
176
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
177
|
+
|
178
|
+
params = params_for_registration(email: email, passkey_label: " ", raw_credential: raw_credential)
|
179
|
+
|
180
|
+
assert_no_difference "#{passkey_class}.count" do
|
181
|
+
assert_no_difference "#{resource_class}.count" do
|
182
|
+
create_resource_action(params: params)
|
183
|
+
assert_response :bad_request
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
test "create registration action requires a passkey_credential" do
|
189
|
+
email = Faker::Internet.email
|
190
|
+
passkey_label = SecureRandom.hex
|
191
|
+
|
192
|
+
params = params_for_registration_challenge(email: email, passkey_label: passkey_label)
|
193
|
+
new_registration_challenge_action(params: params)
|
194
|
+
assert_response :ok
|
195
|
+
webauthn_id = response.parsed_body["user"]["id"]
|
196
|
+
user_data = expected_user_data_for_challenge(email: email, webauthn_id: webauthn_id, name: email)
|
197
|
+
|
198
|
+
assert_passkey_registration_challenge(
|
199
|
+
data: response.parsed_body,
|
200
|
+
stored_challenge: expected_stored_challenge,
|
201
|
+
relying_party_data: expected_relying_party_data,
|
202
|
+
user_data: user_data,
|
203
|
+
credentials_to_exclude: []
|
204
|
+
)
|
205
|
+
|
206
|
+
client = webauthn_client
|
207
|
+
challenge = expected_stored_challenge
|
208
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
209
|
+
|
210
|
+
params = params_for_registration(email: email, passkey_label: passkey_label, raw_credential: " ")
|
211
|
+
|
212
|
+
assert_no_difference "#{passkey_class}.count" do
|
213
|
+
assert_no_difference "#{resource_class}.count" do
|
214
|
+
assert_raises NoMethodError do
|
215
|
+
create_resource_action(params: params)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Controllers::Registrations::Update
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "edit renders successfully for the resource" do
|
8
|
+
sign_in_as_resource
|
9
|
+
edit_registration_action
|
10
|
+
assert_response :ok
|
11
|
+
end
|
12
|
+
|
13
|
+
test "edit does not render a different resource" do
|
14
|
+
sign_in_as_resource
|
15
|
+
attempt_to_edit_other_resource_action
|
16
|
+
assert_response :ok
|
17
|
+
assert_not_includes response.body, other_resource.email
|
18
|
+
end
|
19
|
+
|
20
|
+
test "update updates the resource after reauthentication" do
|
21
|
+
sign_in_as_resource
|
22
|
+
|
23
|
+
client = webauthn_client
|
24
|
+
create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
|
25
|
+
|
26
|
+
new_reauthentication_challenge_action
|
27
|
+
assert_response :ok
|
28
|
+
assert_reauthentication_token_challenge
|
29
|
+
|
30
|
+
challenge = response.parsed_body["challenge"]
|
31
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
32
|
+
|
33
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
34
|
+
assert_response :ok
|
35
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
36
|
+
assert_nil expected_stored_reauthentication_challenge
|
37
|
+
|
38
|
+
new_email = Faker::Internet.email
|
39
|
+
|
40
|
+
reauthentication_token = response.parsed_body["reauthentication_token"]
|
41
|
+
params = params_for_updating_resource(email: new_email, reauthentication_token: reauthentication_token)
|
42
|
+
|
43
|
+
assert_no_difference "#{resource_class}.count" do
|
44
|
+
update_registration_action(params: params)
|
45
|
+
assert_json_redirected_to expected_update_success_url
|
46
|
+
end
|
47
|
+
|
48
|
+
resource_instance.reload
|
49
|
+
assert_equal new_email, resource_instance.email
|
50
|
+
end
|
51
|
+
|
52
|
+
test "update requires a reauthentication token" do
|
53
|
+
sign_in_as_resource
|
54
|
+
|
55
|
+
client = webauthn_client
|
56
|
+
create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
|
57
|
+
|
58
|
+
new_reauthentication_challenge_action
|
59
|
+
assert_response :ok
|
60
|
+
assert_reauthentication_token_challenge
|
61
|
+
|
62
|
+
challenge = response.parsed_body["challenge"]
|
63
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
64
|
+
|
65
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
66
|
+
assert_response :ok
|
67
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
68
|
+
assert_nil expected_stored_reauthentication_challenge
|
69
|
+
|
70
|
+
old_email = resource_instance.email
|
71
|
+
new_email = Faker::Internet.email
|
72
|
+
|
73
|
+
reauthentication_token = ""
|
74
|
+
params = params_for_updating_resource(email: new_email, reauthentication_token: reauthentication_token)
|
75
|
+
|
76
|
+
assert_no_difference "#{resource_class}.count" do
|
77
|
+
update_registration_action(params: params)
|
78
|
+
assert_response :bad_request
|
79
|
+
assert_not_reauthenticated_message
|
80
|
+
end
|
81
|
+
|
82
|
+
resource_instance.reload
|
83
|
+
assert_equal old_email, resource_instance.email
|
84
|
+
end
|
85
|
+
|
86
|
+
test "update requires the reauthentication token to match the stored value" do
|
87
|
+
sign_in_as_resource
|
88
|
+
|
89
|
+
client = webauthn_client
|
90
|
+
create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
|
91
|
+
|
92
|
+
new_reauthentication_challenge_action
|
93
|
+
assert_response :ok
|
94
|
+
assert_reauthentication_token_challenge
|
95
|
+
|
96
|
+
challenge = response.parsed_body["challenge"]
|
97
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
98
|
+
|
99
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
100
|
+
assert_response :ok
|
101
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
102
|
+
assert_nil expected_stored_reauthentication_challenge
|
103
|
+
|
104
|
+
old_email = resource_instance.email
|
105
|
+
new_email = Faker::Internet.email
|
106
|
+
|
107
|
+
reauthentication_token = SecureRandom.hex
|
108
|
+
params = params_for_updating_resource(email: new_email, reauthentication_token: reauthentication_token)
|
109
|
+
|
110
|
+
assert_no_difference "#{resource_class}.count" do
|
111
|
+
update_registration_action(params: params)
|
112
|
+
assert_response :bad_request
|
113
|
+
assert_not_reauthenticated_message
|
114
|
+
end
|
115
|
+
|
116
|
+
resource_instance.reload
|
117
|
+
assert_equal old_email, resource_instance.email
|
118
|
+
end
|
119
|
+
|
120
|
+
test "update renders an error if the email is missing" do
|
121
|
+
sign_in_as_resource
|
122
|
+
|
123
|
+
client = webauthn_client
|
124
|
+
create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
|
125
|
+
|
126
|
+
new_reauthentication_challenge_action
|
127
|
+
assert_response :ok
|
128
|
+
assert_reauthentication_token_challenge
|
129
|
+
|
130
|
+
challenge = response.parsed_body["challenge"]
|
131
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
132
|
+
|
133
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
134
|
+
assert_response :ok
|
135
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
136
|
+
assert_nil expected_stored_reauthentication_challenge
|
137
|
+
|
138
|
+
old_email = resource_instance.email
|
139
|
+
new_email = " "
|
140
|
+
|
141
|
+
reauthentication_token = response.parsed_body["reauthentication_token"]
|
142
|
+
params = params_for_updating_resource(email: new_email, reauthentication_token: reauthentication_token)
|
143
|
+
|
144
|
+
assert_no_difference "#{resource_class}.count" do
|
145
|
+
update_registration_action(params: params)
|
146
|
+
assert_response :unprocessable_entity
|
147
|
+
assert_form_error_for_email(message: "can't be blank")
|
148
|
+
end
|
149
|
+
|
150
|
+
resource_instance.reload
|
151
|
+
assert_equal old_email, resource_instance.email
|
152
|
+
end
|
153
|
+
|
154
|
+
test "update renders an error if the email is a duplicate" do
|
155
|
+
sign_in_as_resource
|
156
|
+
|
157
|
+
client = webauthn_client
|
158
|
+
create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
|
159
|
+
|
160
|
+
new_reauthentication_challenge_action
|
161
|
+
assert_response :ok
|
162
|
+
assert_reauthentication_token_challenge
|
163
|
+
|
164
|
+
challenge = response.parsed_body["challenge"]
|
165
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
166
|
+
|
167
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
168
|
+
assert_response :ok
|
169
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
170
|
+
assert_nil expected_stored_reauthentication_challenge
|
171
|
+
|
172
|
+
old_email = resource_instance.email
|
173
|
+
new_email = other_resource.email
|
174
|
+
|
175
|
+
reauthentication_token = response.parsed_body["reauthentication_token"]
|
176
|
+
params = params_for_updating_resource(email: new_email, reauthentication_token: reauthentication_token)
|
177
|
+
|
178
|
+
assert_no_difference "#{resource_class}.count" do
|
179
|
+
update_registration_action(params: params)
|
180
|
+
assert_response :unprocessable_entity
|
181
|
+
assert_form_error_for_email(message: "has already been taken")
|
182
|
+
end
|
183
|
+
|
184
|
+
resource_instance.reload
|
185
|
+
assert_equal old_email, resource_instance.email
|
186
|
+
end
|
187
|
+
|
188
|
+
test "update ignores when trying to update a different resource" do
|
189
|
+
sign_in_as_resource
|
190
|
+
|
191
|
+
client = webauthn_client
|
192
|
+
create_passkey_for_user_and_return_webauthn_credential(user: resource_instance)
|
193
|
+
|
194
|
+
new_reauthentication_challenge_action
|
195
|
+
assert_response :ok
|
196
|
+
assert_reauthentication_token_challenge
|
197
|
+
|
198
|
+
challenge = response.parsed_body["challenge"]
|
199
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
200
|
+
|
201
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
202
|
+
assert_response :ok
|
203
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
204
|
+
assert_nil expected_stored_reauthentication_challenge
|
205
|
+
|
206
|
+
new_email = Faker::Internet.email
|
207
|
+
|
208
|
+
reauthentication_token = response.parsed_body["reauthentication_token"]
|
209
|
+
params = params_trying_to_update_other_resource(email: new_email, reauthentication_token: reauthentication_token)
|
210
|
+
|
211
|
+
assert_no_difference "#{resource_class}.count" do
|
212
|
+
update_registration_action(params: params)
|
213
|
+
assert_json_redirected_to expected_update_success_url
|
214
|
+
end
|
215
|
+
|
216
|
+
resource_instance.reload
|
217
|
+
assert_equal new_email, resource_instance.email
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Controllers::Sessions::Base
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "new: renders successfully" do
|
8
|
+
get_new_session_url
|
9
|
+
assert_response :ok
|
10
|
+
end
|
11
|
+
|
12
|
+
test "new_challenge: does not require the resource to be authenticated and sets the session variable that stores the new challenge" do
|
13
|
+
get root_url
|
14
|
+
assert_resource_not_signed_in
|
15
|
+
issue_new_challenge_action
|
16
|
+
assert_response :ok
|
17
|
+
assert_equal get_session_challenge, response.parsed_body["challenge"]
|
18
|
+
end
|
19
|
+
|
20
|
+
test "new_challenge: overrides the session variable that stores the new challenge" do
|
21
|
+
issue_new_challenge_action
|
22
|
+
assert_response :ok
|
23
|
+
|
24
|
+
old_session_challenge = get_session_challenge
|
25
|
+
|
26
|
+
issue_new_challenge_action
|
27
|
+
assert_response :ok
|
28
|
+
|
29
|
+
assert_not_equal old_session_challenge, get_session_challenge
|
30
|
+
assert_not_equal old_session_challenge, response.parsed_body["challenge"]
|
31
|
+
end
|
32
|
+
|
33
|
+
test """create:
|
34
|
+
- logs the resource in
|
35
|
+
- clears the challenge session variable
|
36
|
+
- sets the remember cookie value
|
37
|
+
""" do
|
38
|
+
issue_new_challenge_action
|
39
|
+
assert_response :ok
|
40
|
+
|
41
|
+
assert_passkey_authentication_challenge(
|
42
|
+
data: response.parsed_body,
|
43
|
+
stored_challenge: expected_stored_challenge,
|
44
|
+
credentials_to_allow: []
|
45
|
+
)
|
46
|
+
|
47
|
+
challenge = response.parsed_body["challenge"]
|
48
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
49
|
+
|
50
|
+
assert_nil remember_cookie_value
|
51
|
+
|
52
|
+
authenticate_action(params: {resource_key => {passkey_credential: credential.to_json}})
|
53
|
+
assert_redirected_to root_url
|
54
|
+
|
55
|
+
assert_resource_signed_in
|
56
|
+
assert_resource_remembered
|
57
|
+
assert_nil get_session_challenge
|
58
|
+
end
|
59
|
+
|
60
|
+
test """create:
|
61
|
+
- raises an error if the challenge does not match what is in the session
|
62
|
+
- clears the challenge session variable
|
63
|
+
""" do
|
64
|
+
issue_new_challenge_action
|
65
|
+
assert_response :ok
|
66
|
+
|
67
|
+
assert_passkey_authentication_challenge(
|
68
|
+
data: response.parsed_body,
|
69
|
+
stored_challenge: expected_stored_challenge,
|
70
|
+
credentials_to_allow: []
|
71
|
+
)
|
72
|
+
|
73
|
+
challenge = SecureRandom.hex
|
74
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
75
|
+
|
76
|
+
authenticate_action(params: {resource_key => {passkey_credential: credential.to_json}})
|
77
|
+
assert_response :unprocessable_entity
|
78
|
+
|
79
|
+
assert_nil get_session_challenge
|
80
|
+
assert_resource_not_signed_in
|
81
|
+
end
|
82
|
+
|
83
|
+
test """create:
|
84
|
+
- raises an error if the credential is invalid
|
85
|
+
- clears the challenge session variable
|
86
|
+
""" do
|
87
|
+
issue_new_challenge_action
|
88
|
+
assert_response :ok
|
89
|
+
|
90
|
+
assert_passkey_authentication_challenge(
|
91
|
+
data: response.parsed_body,
|
92
|
+
stored_challenge: expected_stored_challenge,
|
93
|
+
credentials_to_allow: []
|
94
|
+
)
|
95
|
+
|
96
|
+
challenge = response.parsed_body["challenge"]
|
97
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
98
|
+
|
99
|
+
invalidate_all_credentials
|
100
|
+
|
101
|
+
authenticate_action(params: {resource_key => {passkey_credential: credential.to_json}})
|
102
|
+
assert_response :unprocessable_entity
|
103
|
+
|
104
|
+
assert_nil get_session_challenge
|
105
|
+
assert_resource_not_signed_in
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|