practical 0.1.0 → 3.0.0.pre.alpha1
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 +4 -4
- data/README.md +4 -4
- data/app/components/practical/views/flash_messages_component.rb +0 -1
- data/app/components/practical/views/form/fallback_errors_section_component.rb +5 -3
- data/app/components/practical/views/form/option_label_component.rb +0 -1
- data/app/components/practical/views/navigation/breadcrumb_item_component.rb +0 -1
- data/app/components/practical/views/navigation/breadcrumbs_component.rb +2 -1
- data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/emergency_registrations.rb +2 -2
- data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/web_authn_debug_context.rb +1 -1
- data/app/concerns/practical/memberships/controllers/membership_invitations/register_with_passkey.rb +92 -0
- data/app/lib/practical/forms/datatables/base.rb +80 -0
- data/app/lib/practical/loaders/base.rb +44 -0
- data/app/lib/practical/relation_builders/base.rb +35 -0
- data/app/lib/practical/test/shared/attachment/models/attachment/base.rb +123 -0
- data/app/lib/practical/test/shared/attachment/models/attachment/for_organization.rb +39 -0
- data/app/lib/practical/test/shared/attachment/models/organization/has_attachments.rb +12 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +9 -6
- data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/cross_pollination.rb +49 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/passkey_management/base.rb +508 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +27 -9
- data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/cross_pollination.rb +19 -0
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +26 -8
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +3 -2
- data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +55 -19
- data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/cross_pollination.rb +29 -0
- data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +0 -1
- data/app/lib/practical/test/shared/auth/passkeys/models/{passkey.rb → passkey/base.rb} +1 -1
- data/app/lib/practical/test/shared/auth/passkeys/models/passkey/emergency_registration.rb +23 -0
- data/app/lib/practical/test/shared/auth/passkeys/models/{resource_with_passkeys.rb → resource_with_passkeys/base.rb} +1 -1
- data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys/emergency_registration.rb +41 -0
- data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/base.rb +165 -0
- data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/register_with_passkey.rb +417 -0
- data/app/lib/practical/test/shared/memberships/controllers/organization/membership.rb +400 -0
- data/app/lib/practical/test/shared/memberships/controllers/organization/membership_invitation.rb +148 -0
- data/app/lib/practical/test/shared/memberships/controllers/user/membership.rb +119 -0
- data/app/lib/practical/test/shared/memberships/controllers/user/membership_invitation.rb +57 -0
- data/app/lib/practical/test/shared/memberships/forms/create_new_user_with_membership_invitation.rb +197 -0
- data/app/lib/practical/test/shared/memberships/forms/organization/membership.rb +162 -0
- data/app/lib/practical/test/shared/memberships/forms/organization/new_membership_invitation.rb +195 -0
- data/app/lib/practical/test/shared/memberships/forms/user/membership.rb +87 -0
- data/app/lib/practical/test/shared/memberships/models/membership/base.rb +45 -0
- data/app/lib/practical/test/shared/memberships/models/membership_invitation/base.rb +85 -0
- data/app/lib/practical/test/shared/memberships/models/membership_invitation/sending.rb +76 -0
- data/app/lib/practical/test/shared/memberships/models/membership_invitation/use_for_and_notify.rb +55 -0
- data/app/lib/practical/test/shared/memberships/models/organization/base.rb +25 -0
- data/app/lib/practical/test/shared/memberships/models/user/base.rb +23 -0
- data/app/lib/practical/test/shared/memberships/policies/organization/base_resource.rb +29 -0
- data/app/lib/practical/test/shared/memberships/policies/organization/membership.rb +103 -0
- data/app/lib/practical/test/shared/memberships/policies/organization/membership_invitation.rb +94 -0
- data/app/lib/practical/test/shared/memberships/policies/organization/resource/inherits.rb +10 -0
- data/app/lib/practical/test/shared/memberships/policies/organization.rb +70 -0
- data/app/lib/practical/test/shared/memberships/policies/user/membership.rb +78 -0
- data/app/lib/practical/test/shared/memberships/policies/user/membership_invitation.rb +31 -0
- data/app/lib/practical/test/shared/models/normalized_email.rb +0 -1
- data/app/lib/practical/test/shared/policies/user/base.rb +14 -0
- data/app/lib/practical/views/error_handling.rb +2 -0
- data/app/lib/practical/views/error_response.rb +27 -0
- data/app/lib/practical/views/form_builders/base.rb +5 -4
- data/app/lib/practical/views/form_builders/collection_option.rb +5 -0
- data/app/lib/practical/views/icon_set.rb +12 -6
- data/config/locales/auth.en.yml +18 -0
- data/config/locales/memberships.en.yml +129 -0
- data/db/seeds/memberships/default.rb +68 -0
- data/db/seeds/moderators/default.rb +36 -0
- data/db/seeds/setup.rb +16 -0
- data/db/seeds/test/cases/membership_invitations.rb +31 -0
- data/db/seeds/users/default.rb +17 -15
- data/lib/generators/practical/test/shared_test/shared_test_generator.rb +2 -0
- data/lib/practical/framework/engine.rb +8 -0
- data/lib/practical/helpers/honeybadger_helper.rb +11 -0
- data/lib/practical/helpers/selector_helper.rb +8 -0
- data/lib/practical/version.rb +1 -1
- data/lib/practical/views/element_helper.rb +2 -0
- data/lib/practical/views/theme_helper.rb +13 -0
- data/lib/practical.rb +4 -1
- data/lib/tasks/practical/utility.rake +20 -0
- metadata +54 -11
- data/lib/tasks/practical/framework_tasks.rake +0 -6
@@ -0,0 +1,508 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Controllers::PasskeyManagement::Base
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "new create challenge creates a new registration challenge that excludes any existing credentials" do
|
8
|
+
sign_in_as_resource
|
9
|
+
params = params_for_create_passkey_challenge(label: Faker::Computer.os)
|
10
|
+
assert_create_challenge_authorized do
|
11
|
+
new_create_challenge_action(params: params)
|
12
|
+
end
|
13
|
+
assert_response :ok
|
14
|
+
assert_new_creation_challenge
|
15
|
+
end
|
16
|
+
|
17
|
+
test "new destroy challenge creates a new authentication challenge that includes all passkeys but the target passkey" do
|
18
|
+
sign_in_as_resource
|
19
|
+
create_additional_passkeys
|
20
|
+
assert_destroy_challenge_authorized do
|
21
|
+
new_destroy_challenge_action(target_passkey: target_passkey)
|
22
|
+
end
|
23
|
+
|
24
|
+
assert_response :ok
|
25
|
+
assert_new_destroy_challenge
|
26
|
+
end
|
27
|
+
|
28
|
+
test "new destroy challenge returns an error if there's only 1 passkey" do
|
29
|
+
sign_in_as_resource
|
30
|
+
new_destroy_challenge_action(target_passkey: target_passkey)
|
31
|
+
|
32
|
+
assert_response :bad_request
|
33
|
+
assert_one_passkey_error_message
|
34
|
+
end
|
35
|
+
|
36
|
+
test "create adds the given credential as a passkey after reauthenticating" do
|
37
|
+
sign_in_as_resource
|
38
|
+
|
39
|
+
client = resource_webauthn_client
|
40
|
+
create_passkey_for_resource_and_return_webauthn_credential(resource: resource_instance)
|
41
|
+
|
42
|
+
assert_reauthentication_challenge_authorized do
|
43
|
+
new_reauthentication_challenge_action
|
44
|
+
end
|
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
|
+
assert_reauthentication_authorized do
|
52
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
53
|
+
end
|
54
|
+
assert_response :ok
|
55
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
56
|
+
assert_nil expected_stored_reauthentication_challenge
|
57
|
+
|
58
|
+
params = params_for_create_passkey_challenge(label: Faker::Computer.os)
|
59
|
+
assert_create_challenge_authorized do
|
60
|
+
new_create_challenge_action(params: params)
|
61
|
+
end
|
62
|
+
assert_response :ok
|
63
|
+
assert_new_creation_challenge
|
64
|
+
|
65
|
+
challenge = expected_creation_stored_challenge
|
66
|
+
reauthentication_token = expected_stored_reauthentication_token
|
67
|
+
|
68
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
69
|
+
label = Faker::Computer.os
|
70
|
+
|
71
|
+
params = params_for_creating_passkey(label: label, raw_credential: raw_credential,
|
72
|
+
reauthentication_token: reauthentication_token)
|
73
|
+
|
74
|
+
assert_difference "#{passkey_class}.count", +1 do
|
75
|
+
assert_create_authorized do
|
76
|
+
create_passkey_action(params: params)
|
77
|
+
assert_create_redirect
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
credential = hydrate_response_from_raw_credential(client: client, relying_party: webauthn_relying_party,
|
82
|
+
raw_credential: raw_credential).credential
|
83
|
+
|
84
|
+
new_passkey = resource_instance.passkeys.last
|
85
|
+
assert_equal label, new_passkey.label
|
86
|
+
assert_equal Base64.strict_encode64(credential.id), new_passkey.external_id
|
87
|
+
assert_not_nil new_passkey.public_key
|
88
|
+
assert_nil new_passkey.last_used_at
|
89
|
+
end
|
90
|
+
|
91
|
+
test "create does not allow overriding who the passkey is for" do
|
92
|
+
sign_in_as_resource
|
93
|
+
|
94
|
+
client = resource_webauthn_client
|
95
|
+
create_passkey_for_resource_and_return_webauthn_credential(resource: resource_instance)
|
96
|
+
|
97
|
+
assert_reauthentication_challenge_authorized do
|
98
|
+
new_reauthentication_challenge_action
|
99
|
+
end
|
100
|
+
assert_response :ok
|
101
|
+
assert_reauthentication_token_challenge
|
102
|
+
|
103
|
+
challenge = response.parsed_body["challenge"]
|
104
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
105
|
+
|
106
|
+
assert_reauthentication_authorized do
|
107
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
108
|
+
end
|
109
|
+
assert_response :ok
|
110
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
111
|
+
assert_nil expected_stored_reauthentication_challenge
|
112
|
+
|
113
|
+
params = params_for_create_passkey_challenge(label: Faker::Computer.os)
|
114
|
+
assert_create_challenge_authorized do
|
115
|
+
new_create_challenge_action(params: params)
|
116
|
+
end
|
117
|
+
assert_response :ok
|
118
|
+
assert_new_creation_challenge
|
119
|
+
|
120
|
+
challenge = expected_creation_stored_challenge
|
121
|
+
reauthentication_token = expected_stored_reauthentication_token
|
122
|
+
|
123
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
124
|
+
label = Faker::Computer.os
|
125
|
+
|
126
|
+
params = params_that_try_to_override_owner_when_creating_passkey(label: label, raw_credential: raw_credential,
|
127
|
+
reauthentication_token: reauthentication_token)
|
128
|
+
|
129
|
+
assert_difference "#{passkey_class}.count", +1 do
|
130
|
+
assert_create_authorized do
|
131
|
+
create_passkey_action(params: params)
|
132
|
+
assert_create_redirect
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
credential = hydrate_response_from_raw_credential(client: client, relying_party: webauthn_relying_party,
|
137
|
+
raw_credential: raw_credential).credential
|
138
|
+
|
139
|
+
new_passkey = resource_instance.passkeys.last
|
140
|
+
assert_equal label, new_passkey.label
|
141
|
+
assert_equal Base64.strict_encode64(credential.id), new_passkey.external_id
|
142
|
+
assert_not_nil new_passkey.public_key
|
143
|
+
assert_nil new_passkey.last_used_at
|
144
|
+
end
|
145
|
+
|
146
|
+
test "create requires a reauthentication token to be provided" do
|
147
|
+
sign_in_as_resource
|
148
|
+
|
149
|
+
client = resource_webauthn_client
|
150
|
+
create_passkey_for_resource_and_return_webauthn_credential(resource: resource_instance)
|
151
|
+
|
152
|
+
assert_reauthentication_challenge_authorized do
|
153
|
+
new_reauthentication_challenge_action
|
154
|
+
end
|
155
|
+
assert_response :ok
|
156
|
+
assert_reauthentication_token_challenge
|
157
|
+
|
158
|
+
challenge = response.parsed_body["challenge"]
|
159
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
160
|
+
|
161
|
+
assert_reauthentication_authorized do
|
162
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
163
|
+
end
|
164
|
+
assert_response :ok
|
165
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
166
|
+
assert_nil expected_stored_reauthentication_challenge
|
167
|
+
|
168
|
+
params = params_for_create_passkey_challenge(label: Faker::Computer.os)
|
169
|
+
assert_create_challenge_authorized do
|
170
|
+
new_create_challenge_action(params: params)
|
171
|
+
end
|
172
|
+
assert_response :ok
|
173
|
+
assert_new_creation_challenge
|
174
|
+
|
175
|
+
challenge = expected_creation_stored_challenge
|
176
|
+
reauthentication_token = " "
|
177
|
+
|
178
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
179
|
+
label = Faker::Computer.os
|
180
|
+
|
181
|
+
params = params_for_creating_passkey(label: label, raw_credential: raw_credential,
|
182
|
+
reauthentication_token: reauthentication_token)
|
183
|
+
|
184
|
+
assert_no_difference "#{passkey_class}.count", +1 do
|
185
|
+
create_passkey_action(params: params)
|
186
|
+
assert_response :bad_request
|
187
|
+
assert_not_reauthenticated_message
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
test "create returns an error if the reauthentication_token is bad" do
|
192
|
+
sign_in_as_resource
|
193
|
+
|
194
|
+
client = resource_webauthn_client
|
195
|
+
create_passkey_for_resource_and_return_webauthn_credential(resource: resource_instance)
|
196
|
+
|
197
|
+
assert_reauthentication_challenge_authorized do
|
198
|
+
new_reauthentication_challenge_action
|
199
|
+
end
|
200
|
+
assert_response :ok
|
201
|
+
assert_reauthentication_token_challenge
|
202
|
+
|
203
|
+
challenge = response.parsed_body["challenge"]
|
204
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
205
|
+
|
206
|
+
assert_reauthentication_authorized do
|
207
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
208
|
+
end
|
209
|
+
assert_response :ok
|
210
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
211
|
+
assert_nil expected_stored_reauthentication_challenge
|
212
|
+
|
213
|
+
params = params_for_create_passkey_challenge(label: Faker::Computer.os)
|
214
|
+
assert_create_challenge_authorized do
|
215
|
+
new_create_challenge_action(params: params)
|
216
|
+
end
|
217
|
+
assert_response :ok
|
218
|
+
assert_new_creation_challenge
|
219
|
+
|
220
|
+
challenge = expected_creation_stored_challenge
|
221
|
+
reauthentication_token = SecureRandom.hex
|
222
|
+
|
223
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
224
|
+
label = Faker::Computer.os
|
225
|
+
|
226
|
+
params = params_for_creating_passkey(label: label, raw_credential: raw_credential,
|
227
|
+
reauthentication_token: reauthentication_token)
|
228
|
+
|
229
|
+
assert_no_difference "#{passkey_class}.count", +1 do
|
230
|
+
create_passkey_action(params: params)
|
231
|
+
assert_response :bad_request
|
232
|
+
assert_not_reauthenticated_message
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
test "create returns an error if the passkey_label is missing" do
|
237
|
+
sign_in_as_resource
|
238
|
+
|
239
|
+
client = resource_webauthn_client
|
240
|
+
create_passkey_for_resource_and_return_webauthn_credential(resource: resource_instance)
|
241
|
+
|
242
|
+
assert_reauthentication_challenge_authorized do
|
243
|
+
new_reauthentication_challenge_action
|
244
|
+
end
|
245
|
+
assert_response :ok
|
246
|
+
assert_reauthentication_token_challenge
|
247
|
+
|
248
|
+
challenge = response.parsed_body["challenge"]
|
249
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
250
|
+
|
251
|
+
assert_reauthentication_authorized do
|
252
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
253
|
+
end
|
254
|
+
assert_response :ok
|
255
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
256
|
+
assert_nil expected_stored_reauthentication_challenge
|
257
|
+
|
258
|
+
params = params_for_create_passkey_challenge(label: Faker::Computer.os)
|
259
|
+
assert_create_challenge_authorized do
|
260
|
+
new_create_challenge_action(params: params)
|
261
|
+
end
|
262
|
+
assert_response :ok
|
263
|
+
assert_new_creation_challenge
|
264
|
+
|
265
|
+
challenge = expected_creation_stored_challenge
|
266
|
+
reauthentication_token = expected_stored_reauthentication_token
|
267
|
+
|
268
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
269
|
+
label = " "
|
270
|
+
|
271
|
+
params = params_for_creating_passkey(label: label, raw_credential: raw_credential,
|
272
|
+
reauthentication_token: reauthentication_token)
|
273
|
+
|
274
|
+
assert_no_difference "#{passkey_class}.count", +1 do
|
275
|
+
assert_create_authorized do
|
276
|
+
create_passkey_action(params: params)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
assert_response :unprocessable_entity
|
281
|
+
assert_form_error_for_blank_label
|
282
|
+
end
|
283
|
+
|
284
|
+
test "create returns an error if the passkey_label is a duplicate" do
|
285
|
+
sign_in_as_resource
|
286
|
+
|
287
|
+
client = resource_webauthn_client
|
288
|
+
create_passkey_for_resource_and_return_webauthn_credential(resource: resource_instance)
|
289
|
+
|
290
|
+
assert_reauthentication_challenge_authorized do
|
291
|
+
new_reauthentication_challenge_action
|
292
|
+
end
|
293
|
+
assert_response :ok
|
294
|
+
assert_reauthentication_token_challenge
|
295
|
+
|
296
|
+
challenge = response.parsed_body["challenge"]
|
297
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
298
|
+
|
299
|
+
assert_reauthentication_authorized do
|
300
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
301
|
+
end
|
302
|
+
assert_response :ok
|
303
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
304
|
+
assert_nil expected_stored_reauthentication_challenge
|
305
|
+
|
306
|
+
params = params_for_create_passkey_challenge(label: Faker::Computer.os)
|
307
|
+
assert_create_challenge_authorized do
|
308
|
+
new_create_challenge_action(params: params)
|
309
|
+
end
|
310
|
+
assert_response :ok
|
311
|
+
assert_new_creation_challenge
|
312
|
+
|
313
|
+
challenge = expected_creation_stored_challenge
|
314
|
+
reauthentication_token = expected_stored_reauthentication_token
|
315
|
+
|
316
|
+
raw_credential = create_credential_and_return_payload_from_challenge(client: client, challenge: challenge)
|
317
|
+
label = target_passkey.label
|
318
|
+
|
319
|
+
params = params_for_creating_passkey(label: label, raw_credential: raw_credential,
|
320
|
+
reauthentication_token: reauthentication_token)
|
321
|
+
|
322
|
+
assert_no_difference "#{passkey_class}.count", +1 do
|
323
|
+
assert_create_authorized do
|
324
|
+
create_passkey_action(params: params)
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
assert_response :unprocessable_entity
|
329
|
+
assert_form_error_for_taken_label
|
330
|
+
end
|
331
|
+
|
332
|
+
test "destroy deletes the given passkey" do
|
333
|
+
sign_in_as_resource
|
334
|
+
|
335
|
+
client = resource_webauthn_client
|
336
|
+
create_passkey_for_resource_and_return_webauthn_credential(resource: resource_instance)
|
337
|
+
|
338
|
+
assert_destroy_challenge_authorized do
|
339
|
+
new_destroy_challenge_action(target_passkey: target_passkey)
|
340
|
+
end
|
341
|
+
assert_response :ok
|
342
|
+
assert_new_destroy_challenge
|
343
|
+
|
344
|
+
challenge = response.parsed_body["challenge"]
|
345
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
346
|
+
|
347
|
+
assert_reauthentication_authorized do
|
348
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
349
|
+
end
|
350
|
+
assert_response :ok
|
351
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
352
|
+
assert_nil expected_stored_reauthentication_challenge
|
353
|
+
|
354
|
+
passkey = target_passkey
|
355
|
+
reauthentication_token = response.parsed_body["reauthentication_token"]
|
356
|
+
params = params_for_destroying_passkey(reauthentication_token: reauthentication_token)
|
357
|
+
|
358
|
+
assert_difference "#{passkey_class}.count", -1 do
|
359
|
+
assert_destroy_authorized do
|
360
|
+
destroy_passkey_action(target_passkey: passkey, params: params)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
assert_nil passkey_class.find_by(id: passkey.id)
|
365
|
+
assert_destroy_redirect
|
366
|
+
end
|
367
|
+
|
368
|
+
test "destroy requires a reauthentication token to be present" do
|
369
|
+
sign_in_as_resource
|
370
|
+
|
371
|
+
client = resource_webauthn_client
|
372
|
+
create_passkey_for_resource_and_return_webauthn_credential(resource: resource_instance)
|
373
|
+
|
374
|
+
assert_destroy_challenge_authorized do
|
375
|
+
new_destroy_challenge_action(target_passkey: target_passkey)
|
376
|
+
end
|
377
|
+
assert_response :ok
|
378
|
+
assert_new_destroy_challenge
|
379
|
+
|
380
|
+
challenge = response.parsed_body["challenge"]
|
381
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
382
|
+
assert_reauthentication_authorized do
|
383
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
384
|
+
end
|
385
|
+
assert_response :ok
|
386
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
387
|
+
assert_nil expected_stored_reauthentication_challenge
|
388
|
+
|
389
|
+
passkey = target_passkey
|
390
|
+
reauthentication_token = " "
|
391
|
+
params = params_for_destroying_passkey(reauthentication_token: reauthentication_token)
|
392
|
+
|
393
|
+
assert_no_difference "#{passkey_class}.count" do
|
394
|
+
destroy_passkey_action(target_passkey: passkey, params: params)
|
395
|
+
assert_response :bad_request
|
396
|
+
assert_not_reauthenticated_message
|
397
|
+
end
|
398
|
+
|
399
|
+
assert_not_nil passkey_class.find_by(id: passkey.id)
|
400
|
+
end
|
401
|
+
|
402
|
+
test "destroy return an error if the reauthentication token is bad" do
|
403
|
+
sign_in_as_resource
|
404
|
+
|
405
|
+
client = resource_webauthn_client
|
406
|
+
create_passkey_for_resource_and_return_webauthn_credential(resource: resource_instance)
|
407
|
+
|
408
|
+
assert_destroy_challenge_authorized do
|
409
|
+
new_destroy_challenge_action(target_passkey: target_passkey)
|
410
|
+
end
|
411
|
+
assert_response :ok
|
412
|
+
assert_new_destroy_challenge
|
413
|
+
|
414
|
+
challenge = response.parsed_body["challenge"]
|
415
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
416
|
+
|
417
|
+
assert_reauthentication_authorized do
|
418
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
419
|
+
end
|
420
|
+
assert_response :ok
|
421
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
422
|
+
assert_nil expected_stored_reauthentication_challenge
|
423
|
+
|
424
|
+
passkey = target_passkey
|
425
|
+
reauthentication_token = SecureRandom.hex
|
426
|
+
params = params_for_destroying_passkey(reauthentication_token: reauthentication_token)
|
427
|
+
|
428
|
+
assert_no_difference "#{passkey_class}.count" do
|
429
|
+
destroy_passkey_action(target_passkey: passkey, params: params)
|
430
|
+
assert_response :bad_request
|
431
|
+
assert_not_reauthenticated_message
|
432
|
+
end
|
433
|
+
|
434
|
+
assert_not_nil passkey_class.find_by(id: passkey.id)
|
435
|
+
end
|
436
|
+
|
437
|
+
test "destroy does not delete a passkey for another resource" do
|
438
|
+
sign_in_as_resource
|
439
|
+
|
440
|
+
client = resource_webauthn_client
|
441
|
+
create_passkey_for_resource_and_return_webauthn_credential(resource: resource_instance)
|
442
|
+
|
443
|
+
assert_destroy_challenge_authorized do
|
444
|
+
new_destroy_challenge_action(target_passkey: target_passkey)
|
445
|
+
end
|
446
|
+
assert_response :ok
|
447
|
+
assert_new_destroy_challenge
|
448
|
+
|
449
|
+
challenge = response.parsed_body["challenge"]
|
450
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
451
|
+
|
452
|
+
assert_reauthentication_authorized do
|
453
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
454
|
+
end
|
455
|
+
assert_response :ok
|
456
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
457
|
+
assert_nil expected_stored_reauthentication_challenge
|
458
|
+
|
459
|
+
passkey = passkey_for_another_resource
|
460
|
+
reauthentication_token = response.parsed_body["reauthentication_token"]
|
461
|
+
params = params_for_destroying_passkey(reauthentication_token: reauthentication_token)
|
462
|
+
|
463
|
+
assert_no_difference "#{passkey_class}.count" do
|
464
|
+
destroy_passkey_action(target_passkey: passkey, params: params)
|
465
|
+
assert_response :not_found
|
466
|
+
end
|
467
|
+
|
468
|
+
assert_not_nil passkey_class.find_by(id: passkey.id)
|
469
|
+
end
|
470
|
+
|
471
|
+
test "destroy returns an error if there's only 1 passkey" do
|
472
|
+
sign_in_as_resource
|
473
|
+
|
474
|
+
client = resource_webauthn_client
|
475
|
+
create_passkey_for_resource_and_return_webauthn_credential(resource: resource_instance)
|
476
|
+
|
477
|
+
assert_destroy_challenge_authorized do
|
478
|
+
new_destroy_challenge_action(target_passkey: target_passkey)
|
479
|
+
end
|
480
|
+
assert_response :ok
|
481
|
+
assert_new_destroy_challenge
|
482
|
+
|
483
|
+
challenge = response.parsed_body["challenge"]
|
484
|
+
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
485
|
+
|
486
|
+
assert_reauthentication_authorized do
|
487
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
488
|
+
end
|
489
|
+
assert_response :ok
|
490
|
+
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
491
|
+
assert_nil expected_stored_reauthentication_challenge
|
492
|
+
|
493
|
+
passkey = target_passkey
|
494
|
+
reauthentication_token = response.parsed_body["reauthentication_token"]
|
495
|
+
params = params_for_destroying_passkey(reauthentication_token: reauthentication_token)
|
496
|
+
|
497
|
+
delete_all_but_target_passkey
|
498
|
+
|
499
|
+
assert_no_difference "#{passkey_class}.count" do
|
500
|
+
destroy_passkey_action(target_passkey: passkey, params: params)
|
501
|
+
assert_response :bad_request
|
502
|
+
assert_one_passkey_error_message
|
503
|
+
end
|
504
|
+
|
505
|
+
assert_not_nil passkey_class.find_by(id: passkey.id)
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
@@ -11,18 +11,24 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Reauthentication::B
|
|
11
11
|
|
12
12
|
test "new_challenge: sets the session variable that stores the new challenge" do
|
13
13
|
sign_in_as_resource
|
14
|
-
|
14
|
+
assert_new_challenge_authorized do
|
15
|
+
issue_new_challenge_action
|
16
|
+
end
|
15
17
|
assert_response :ok
|
16
18
|
assert_equal get_session_challenge, response.parsed_body["challenge"]
|
17
19
|
end
|
18
20
|
|
19
21
|
test "new_challenge: overrides the session variable that stores the new challenge" do
|
20
22
|
sign_in_as_resource
|
21
|
-
|
23
|
+
assert_new_challenge_authorized do
|
24
|
+
issue_new_challenge_action
|
25
|
+
end
|
22
26
|
assert_response :ok
|
23
27
|
old_session_challenge = get_session_challenge
|
24
28
|
|
25
|
-
|
29
|
+
assert_new_challenge_authorized do
|
30
|
+
issue_new_challenge_action
|
31
|
+
end
|
26
32
|
assert_response :ok
|
27
33
|
|
28
34
|
assert_not_equal old_session_challenge, get_session_challenge
|
@@ -41,7 +47,9 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Reauthentication::B
|
|
41
47
|
- stores the reauthentication_token in the session
|
42
48
|
""" do
|
43
49
|
sign_in_as_resource
|
44
|
-
|
50
|
+
assert_new_challenge_authorized do
|
51
|
+
issue_new_challenge_action
|
52
|
+
end
|
45
53
|
assert_response :ok
|
46
54
|
|
47
55
|
assert_passkey_authentication_challenge(
|
@@ -53,7 +61,9 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Reauthentication::B
|
|
53
61
|
challenge = response.parsed_body["challenge"]
|
54
62
|
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
55
63
|
|
56
|
-
|
64
|
+
assert_reauthentication_authorized do
|
65
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
66
|
+
end
|
57
67
|
assert_response :ok
|
58
68
|
|
59
69
|
assert_equal expected_stored_reauthentication_token, response.parsed_body["reauthentication_token"]
|
@@ -66,7 +76,9 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Reauthentication::B
|
|
66
76
|
- does not have a reauthentication_token in the session
|
67
77
|
""" do
|
68
78
|
sign_in_as_resource
|
69
|
-
|
79
|
+
assert_new_challenge_authorized do
|
80
|
+
issue_new_challenge_action
|
81
|
+
end
|
70
82
|
assert_response :ok
|
71
83
|
|
72
84
|
assert_passkey_authentication_challenge(
|
@@ -78,7 +90,9 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Reauthentication::B
|
|
78
90
|
challenge = SecureRandom.hex
|
79
91
|
credential = get_credential_payload_from_challenge(client: client, challenge: challenge)
|
80
92
|
|
81
|
-
|
93
|
+
assert_reauthentication_authorized do
|
94
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
95
|
+
end
|
82
96
|
assert_response :unauthorized
|
83
97
|
|
84
98
|
assert_equal I18n.translate("devise.failure.webauthn_challenge_verification_error"), response.parsed_body["error"]
|
@@ -93,7 +107,9 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Reauthentication::B
|
|
93
107
|
- does not have a reauthentication_token in the session
|
94
108
|
""" do
|
95
109
|
sign_in_as_resource
|
96
|
-
|
110
|
+
assert_new_challenge_authorized do
|
111
|
+
issue_new_challenge_action
|
112
|
+
end
|
97
113
|
assert_response :ok
|
98
114
|
|
99
115
|
assert_passkey_authentication_challenge(
|
@@ -107,7 +123,9 @@ module Practical::Test::Shared::Auth::Passkeys::Controllers::Reauthentication::B
|
|
107
123
|
|
108
124
|
invalidate_all_credentials
|
109
125
|
|
110
|
-
|
126
|
+
assert_reauthentication_authorized do
|
127
|
+
reauthenticate_action(params: {passkey_credential: credential.to_json})
|
128
|
+
end
|
111
129
|
assert_response :unauthorized
|
112
130
|
|
113
131
|
assert_equal I18n.translate("devise.failure.stored_credential_not_found"), response.parsed_body["error"]
|
data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/cross_pollination.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Practical::Test::Shared::Auth::Passkeys::Controllers::Reauthentication::CrossPollination
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
test "new_challenge: does not issue a challenge for another authenticated resource type" do
|
8
|
+
sign_in_as_other_resource
|
9
|
+
issue_new_challenge_action
|
10
|
+
assert_response :not_found
|
11
|
+
end
|
12
|
+
|
13
|
+
test "reauthenticate: does not issue a challenge for another authenticated resource type" do
|
14
|
+
sign_in_as_other_resource
|
15
|
+
reauthenticate_action(params: {})
|
16
|
+
assert_response :not_found
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|