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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/app/components/practical/views/flash_messages_component.rb +0 -1
  4. data/app/components/practical/views/form/fallback_errors_section_component.rb +5 -3
  5. data/app/components/practical/views/form/option_label_component.rb +0 -1
  6. data/app/components/practical/views/navigation/breadcrumb_item_component.rb +0 -1
  7. data/app/components/practical/views/navigation/breadcrumbs_component.rb +2 -1
  8. data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/emergency_registrations.rb +2 -2
  9. data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/web_authn_debug_context.rb +1 -1
  10. data/app/concerns/practical/memberships/controllers/membership_invitations/register_with_passkey.rb +92 -0
  11. data/app/lib/practical/forms/datatables/base.rb +80 -0
  12. data/app/lib/practical/loaders/base.rb +44 -0
  13. data/app/lib/practical/relation_builders/base.rb +35 -0
  14. data/app/lib/practical/test/shared/attachment/models/attachment/base.rb +123 -0
  15. data/app/lib/practical/test/shared/attachment/models/attachment/for_organization.rb +39 -0
  16. data/app/lib/practical/test/shared/attachment/models/organization/has_attachments.rb +12 -0
  17. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +9 -6
  18. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/cross_pollination.rb +49 -0
  19. data/app/lib/practical/test/shared/auth/passkeys/controllers/passkey_management/base.rb +508 -0
  20. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +27 -9
  21. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/cross_pollination.rb +19 -0
  22. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +26 -8
  23. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +3 -2
  24. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +55 -19
  25. data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/cross_pollination.rb +29 -0
  26. data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +0 -1
  27. data/app/lib/practical/test/shared/auth/passkeys/models/{passkey.rb → passkey/base.rb} +1 -1
  28. data/app/lib/practical/test/shared/auth/passkeys/models/passkey/emergency_registration.rb +23 -0
  29. data/app/lib/practical/test/shared/auth/passkeys/models/{resource_with_passkeys.rb → resource_with_passkeys/base.rb} +1 -1
  30. data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys/emergency_registration.rb +41 -0
  31. data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/base.rb +165 -0
  32. data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/register_with_passkey.rb +417 -0
  33. data/app/lib/practical/test/shared/memberships/controllers/organization/membership.rb +400 -0
  34. data/app/lib/practical/test/shared/memberships/controllers/organization/membership_invitation.rb +148 -0
  35. data/app/lib/practical/test/shared/memberships/controllers/user/membership.rb +119 -0
  36. data/app/lib/practical/test/shared/memberships/controllers/user/membership_invitation.rb +57 -0
  37. data/app/lib/practical/test/shared/memberships/forms/create_new_user_with_membership_invitation.rb +197 -0
  38. data/app/lib/practical/test/shared/memberships/forms/organization/membership.rb +162 -0
  39. data/app/lib/practical/test/shared/memberships/forms/organization/new_membership_invitation.rb +195 -0
  40. data/app/lib/practical/test/shared/memberships/forms/user/membership.rb +87 -0
  41. data/app/lib/practical/test/shared/memberships/models/membership/base.rb +45 -0
  42. data/app/lib/practical/test/shared/memberships/models/membership_invitation/base.rb +85 -0
  43. data/app/lib/practical/test/shared/memberships/models/membership_invitation/sending.rb +76 -0
  44. data/app/lib/practical/test/shared/memberships/models/membership_invitation/use_for_and_notify.rb +55 -0
  45. data/app/lib/practical/test/shared/memberships/models/organization/base.rb +25 -0
  46. data/app/lib/practical/test/shared/memberships/models/user/base.rb +23 -0
  47. data/app/lib/practical/test/shared/memberships/policies/organization/base_resource.rb +29 -0
  48. data/app/lib/practical/test/shared/memberships/policies/organization/membership.rb +103 -0
  49. data/app/lib/practical/test/shared/memberships/policies/organization/membership_invitation.rb +94 -0
  50. data/app/lib/practical/test/shared/memberships/policies/organization/resource/inherits.rb +10 -0
  51. data/app/lib/practical/test/shared/memberships/policies/organization.rb +70 -0
  52. data/app/lib/practical/test/shared/memberships/policies/user/membership.rb +78 -0
  53. data/app/lib/practical/test/shared/memberships/policies/user/membership_invitation.rb +31 -0
  54. data/app/lib/practical/test/shared/models/normalized_email.rb +0 -1
  55. data/app/lib/practical/test/shared/policies/user/base.rb +14 -0
  56. data/app/lib/practical/views/error_handling.rb +2 -0
  57. data/app/lib/practical/views/error_response.rb +27 -0
  58. data/app/lib/practical/views/form_builders/base.rb +5 -4
  59. data/app/lib/practical/views/form_builders/collection_option.rb +5 -0
  60. data/app/lib/practical/views/icon_set.rb +12 -6
  61. data/config/locales/auth.en.yml +18 -0
  62. data/config/locales/memberships.en.yml +129 -0
  63. data/db/seeds/memberships/default.rb +68 -0
  64. data/db/seeds/moderators/default.rb +36 -0
  65. data/db/seeds/setup.rb +16 -0
  66. data/db/seeds/test/cases/membership_invitations.rb +31 -0
  67. data/db/seeds/users/default.rb +17 -15
  68. data/lib/generators/practical/test/shared_test/shared_test_generator.rb +2 -0
  69. data/lib/practical/framework/engine.rb +8 -0
  70. data/lib/practical/helpers/honeybadger_helper.rb +11 -0
  71. data/lib/practical/helpers/selector_helper.rb +8 -0
  72. data/lib/practical/version.rb +1 -1
  73. data/lib/practical/views/element_helper.rb +2 -0
  74. data/lib/practical/views/theme_helper.rb +13 -0
  75. data/lib/practical.rb +4 -1
  76. data/lib/tasks/practical/utility.rake +20 -0
  77. metadata +54 -11
  78. 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
- issue_new_challenge_action
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
- issue_new_challenge_action
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
- issue_new_challenge_action
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
- issue_new_challenge_action
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
- reauthenticate_action(params: {passkey_credential: credential.to_json})
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
- issue_new_challenge_action
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
- reauthenticate_action(params: {passkey_credential: credential.to_json})
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
- issue_new_challenge_action
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
- reauthenticate_action(params: {passkey_credential: credential.to_json})
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"]
@@ -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