Skip to content

Commit 1185d97

Browse files
feat: validate userHandle in WebauthnTwoFactorAuthenticatable strategy
1 parent 0c54c11 commit 1185d97

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

lib/devise/strategies/webauthn_two_factor_authenticatable.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ def authenticate!
1616
stored_credential = resource&.webauthn_credentials&.find_by(external_id: credential_from_params.id)
1717

1818
return fail!(:webauthn_credential_not_found) if stored_credential.blank?
19+
if user_handle_mismatch?(credential_from_params, resource)
20+
return fail!(:webauthn_credential_verification_failed)
21+
end
1922

2023
verify_credential(credential_from_params, stored_credential)
2124

@@ -47,6 +50,11 @@ def verify_credential(credential_from_params, stored_credential)
4750
stored_credential.update!(sign_count: credential_from_params.sign_count)
4851
end
4952

53+
def user_handle_mismatch?(credential_from_params, resource)
54+
credential_from_params.user_handle.present? &&
55+
credential_from_params.user_handle != resource.webauthn_id
56+
end
57+
5058
def resource_class
5159
mapping.to
5260
end

spec/requests/devise/two_factor_authentication_spec.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,60 @@ def generate_assertion(fake_client, challenge:, credential:)
164164
expect(controller.current_account).to be_nil
165165
end
166166

167+
it "completes authentication when userHandle matches the authenticated user" do
168+
post account_session_path, params: { account: { email: user.email, password: password } }
169+
170+
expect(response).to redirect_to(new_account_two_factor_authentication_path)
171+
172+
follow_redirect!
173+
174+
expect(response).to have_http_status(:ok)
175+
get account_security_key_authentication_options_path
176+
177+
assertion = client.get(
178+
challenge: session[:two_factor_authentication_challenge],
179+
allow_credentials: [security_key.external_id],
180+
user_handle: WebAuthn.configuration.encoder.decode(user.webauthn_id)
181+
)
182+
183+
expect do
184+
post account_two_factor_authentication_path, params: {
185+
public_key_credential: assertion.to_json
186+
}
187+
188+
expect(response).to redirect_to(root_path)
189+
expect(controller.current_account).to eq(user)
190+
end.to change { security_key.reload.sign_count }.by(1)
191+
end
192+
193+
it "rejects 2FA when userHandle does not match the authenticated user" do
194+
other_user = Account.create!(email: "other@example.com", password: password)
195+
196+
post account_session_path, params: { account: { email: user.email, password: password } }
197+
198+
expect(response).to redirect_to(new_account_two_factor_authentication_path)
199+
200+
follow_redirect!
201+
202+
expect(response).to have_http_status(:ok)
203+
get account_security_key_authentication_options_path
204+
205+
assertion = client.get(
206+
challenge: session[:two_factor_authentication_challenge],
207+
allow_credentials: [security_key.external_id],
208+
user_handle: WebAuthn.configuration.encoder.decode(other_user.webauthn_id)
209+
)
210+
211+
post account_two_factor_authentication_path, params: {
212+
public_key_credential: assertion.to_json
213+
}
214+
215+
expect(response).to have_http_status(:unprocessable_entity)
216+
expect(response.body).to include("Use security key")
217+
expect(flash[:alert]).to eq(I18n.t("devise.failure.webauthn_credential_verification_failed"))
218+
expect(controller.current_account).to be_nil
219+
end
220+
167221
it "re-renders 2FA page when credential param is missing" do
168222
post account_session_path, params: { account: { email: user.email, password: password } }
169223

0 commit comments

Comments
 (0)