feat(auth): add WebAuthn MFA credentials (#1628)

* feat(auth): add WebAuthn MFA credentials

* fix(auth): harden WebAuthn MFA review paths

* fix(auth): polish WebAuthn error handling

* fix(auth): handle duplicate WebAuthn credential races

* fix(auth): permit WebAuthn credential params

* fix(auth): trim WebAuthn registration controller cleanup

* fix(auth): tighten WebAuthn MFA handling

* fix(auth): pin WebAuthn relying party config
This commit is contained in:
ghost
2026-05-03 14:13:28 -06:00
committed by GitHub
parent faf31b9c91
commit 911aa34ba9
29 changed files with 1117 additions and 10 deletions

View File

@@ -10,6 +10,14 @@ class Rack::Attack
request.ip if request.path == "/oauth/token"
end
# Throttle unauthenticated WebAuthn MFA ceremonies similarly to sign-in
# endpoints; registration remains behind normal application authentication.
throttle("mfa/webauthn", limit: 10, period: 1.minute) do |request|
if request.post? && request.path.in?(%w[/mfa/webauthn_options /mfa/verify_webauthn])
request.ip
end
end
# Throttle admin endpoints to prevent brute-force attacks
# More restrictive than general API limits since admin access is sensitive
throttle("admin/ip", limit: 10, period: 1.minute) do |request|

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
Rails.application.configure do
config.x.webauthn = ActiveSupport::OrderedOptions.new
credentials_config = Rails.application.credentials.webauthn || {}
credential_rp_id = credentials_config[:rp_id] || credentials_config["rp_id"]
credential_origins = credentials_config[:allowed_origins] || credentials_config["allowed_origins"]
configured_rp_id = ENV["WEBAUTHN_RP_ID"].presence || credential_rp_id.presence || ENV["APP_DOMAIN"].presence
default_rp_id = Rails.env.test? ? "www.example.com" : "localhost"
rp_id = configured_rp_id.presence || default_rp_id
rp_id = rp_id.to_s.strip.sub(%r{\Ahttps?://}, "").split("/").first.to_s.split(":").first
configured_origins = ENV["WEBAUTHN_ALLOWED_ORIGINS"].presence || credential_origins
allowed_origins = Array(configured_origins)
.flat_map { |origin| origin.to_s.split(",") }
.map { |origin| origin.strip.chomp("/") }
.reject(&:blank?)
if allowed_origins.blank?
allowed_origins = if Rails.env.test?
[ "http://www.example.com" ]
elsif rp_id == "localhost"
[ "http://localhost:3000" ]
else
[ "https://#{rp_id}" ]
end
end
config.x.webauthn.rp_id = rp_id
config.x.webauthn.allowed_origins = allowed_origins
end

View File

@@ -31,8 +31,15 @@ en:
verify_title: 2. Enter Verification Code
verify:
description: Enter the code from your authenticator app to continue
or: or
page_title: Verify Two-Factor Authentication
title: Two-Factor Authentication
verify_button: Verify
webauthn_button: Use passkey or security key
webauthn_unsupported: This browser does not support passkeys or security keys.
verify_code:
invalid_code: Invalid authentication code. Please try again.
verify_webauthn:
invalid_credential: Could not verify that passkey or security key. Please try again.
webauthn_options:
unavailable: No passkeys or security keys are available for this account.

View File

@@ -10,3 +10,25 @@ en:
mfa_description: Add an extra layer of security to your account by requiring
a code from your authenticator app when signing in
mfa_title: Two-Factor Authentication
webauthn_add: Add passkey or security key
webauthn_added: Added %{date}
webauthn_description: Use a passkey, Touch ID, Windows Hello, or a hardware
security key as a second factor when signing in.
webauthn_empty: No passkeys or security keys are registered yet.
webauthn_last_used: Last used %{time_ago} ago
webauthn_name_label: Key name
webauthn_name_placeholder: MacBook Touch ID, YubiKey, etc.
webauthn_remove: Remove
webauthn_remove_confirm: Are you sure you want to remove this passkey or
security key?
webauthn_remove_confirm_body: You will need to register this passkey or
security key again before it can be used for sign-in verification.
webauthn_title: Passkeys and security keys
webauthn_unsupported: This browser does not support passkeys or security
keys.
webauthn_credentials:
default_name: Security key
failure: Could not save that passkey or security key. Please try again.
mfa_required: Enable two-factor authentication before adding a passkey or security
key.
success: Passkey or security key removed.

View File

@@ -118,6 +118,8 @@ Rails.application.routes.draw do
resource :mfa, controller: "mfa", only: [ :new, :create ] do
get :verify
post :verify, to: "mfa#verify_code"
post :webauthn_options
post :verify_webauthn
delete :disable
end
@@ -195,6 +197,9 @@ Rails.application.routes.draw do
end
resource :payment, only: :show
resource :security, only: :show
resources :webauthn_credentials, only: %i[create destroy] do
post :options, on: :collection
end
resources :sso_identities, only: :destroy
resource :api_key, only: [ :show, :new, :create, :destroy ]
resource :ai_prompts, only: :show