Skip to content

Commit 29c74d6

Browse files
smgdkngtclaude
andcommitted
Add ALTCHA proof-of-work challenge to registration form
Blocks automated signups with a privacy-friendly, self-hosted challenge. Uses altcha gem for server-side verification and CDN-hosted widget. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cf629e7 commit 29c74d6

File tree

8 files changed

+51
-1
lines changed

8 files changed

+51
-1
lines changed

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,6 @@ gem "noticed", "~> 3.0"
9696
# TOTP two-factor authentication
9797
gem "rotp", "~> 6.3"
9898
gem "rqrcode", "~> 3.0"
99+
100+
# ALTCHA proof-of-work spam protection
101+
gem "altcha"

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ GEM
7777
uri (>= 0.13.1)
7878
addressable (2.8.8)
7979
public_suffix (>= 2.0.2, < 8.0)
80+
altcha (1.0.0)
8081
ast (2.4.3)
8182
base64 (0.3.0)
8283
bcrypt (3.1.21)
@@ -483,6 +484,7 @@ PLATFORMS
483484
x86_64-linux-musl
484485

485486
DEPENDENCIES
487+
altcha
486488
bcrypt (~> 3.1.7)
487489
bootsnap
488490
brakeman
@@ -534,6 +536,7 @@ CHECKSUMS
534536
activestorage (8.1.2) sha256=8a63a48c3999caeee26a59441f813f94681fc35cc41aba7ce1f836add04fba76
535537
activesupport (8.1.2) sha256=88842578ccd0d40f658289b0e8c842acfe9af751afee2e0744a7873f50b6fdae
536538
addressable (2.8.8) sha256=7c13b8f9536cf6364c03b9d417c19986019e28f7c00ac8132da4eb0fe393b057
539+
altcha (1.0.0) sha256=6f85e1e1b6cc81fd9110c05b5f72e07e1635b75ec62bb4c851a5bdc1b0739f04
537540
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
538541
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
539542
bcrypt (3.1.21) sha256=5964613d750a42c7ee5dc61f7b9336fb6caca429ba4ac9f2011609946e4a2dcf
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
class AltchaController < ApplicationController
4+
allow_unauthenticated_access
5+
6+
def challenge
7+
options = Altcha::ChallengeOptions.new(
8+
hmac_key: altcha_hmac_key,
9+
max_number: 50_000,
10+
expires: Time.now + 5.minutes
11+
)
12+
render json: Altcha.create_challenge(options)
13+
end
14+
15+
private
16+
17+
def altcha_hmac_key
18+
Rails.application.secret_key_base.first(32)
19+
end
20+
end

app/controllers/registrations_controller.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ def new
1010
end
1111

1212
def create
13+
unless verify_altcha
14+
@user = User.new(user_params)
15+
flash.now[:alert] = "Please complete the verification."
16+
return render :new, status: :unprocessable_entity
17+
end
18+
1319
@user = User.new(user_params)
1420

1521
if @user.save
@@ -27,6 +33,20 @@ def create
2733

2834
private
2935

36+
def verify_altcha
37+
payload = params[:altcha]
38+
return false if payload.blank?
39+
40+
parsed = JSON.parse(Base64.decode64(payload), symbolize_names: true)
41+
Altcha.verify_solution(parsed, altcha_hmac_key)
42+
rescue JSON::ParserError, ArgumentError
43+
false
44+
end
45+
46+
def altcha_hmac_key
47+
Rails.application.secret_key_base.first(32)
48+
end
49+
3050
def user_params
3151
params.require(:user).permit(:first_name, :last_name, :email_address, :password, :password_confirmation)
3252
end

app/views/layouts/application.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
<link rel="apple-touch-startup-image" href="/apple-splash-750-1334.png" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
4545
<link rel="apple-touch-startup-image" href="/apple-splash-640-1136.png" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
4646

47+
<script async defer src="https://eu.altcha.org/js/latest/altcha.min.js" type="module"></script>
4748
<%= stylesheet_link_tag "rhino-editor", "data-turbo-track": "reload" %>
4849
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
4950
<%= javascript_importmap_tags %>

app/views/registrations/new.html.erb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
%>
6666
</div>
6767

68+
<altcha-widget challengeurl="/altcha/challenge" name="altcha"></altcha-widget>
69+
6870
<%= render "components/button", text: "Create Account", full_width: true %>
6971
<% end %>
7072
</div>

config/initializers/content_security_policy.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
policy.font_src :self, :data
1111
policy.img_src :self, :data, :https
1212
policy.object_src :none
13-
policy.script_src :self, "https://ga.jspm.io", "https://cdn.jsdelivr.net"
13+
policy.script_src :self, "https://ga.jspm.io", "https://cdn.jsdelivr.net", "https://eu.altcha.org"
1414
policy.style_src :self, "'unsafe-inline'"
1515
policy.frame_src :self
1616
policy.connect_src :self, *[ ENV["LIVEKIT_URL"]&.sub(%r{^https?://}, "wss://") ].compact

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
delete "logout", to: "sessions#destroy"
1616
get "signup", to: "registrations#new"
1717
post "signup", to: "registrations#create"
18+
get "altcha/challenge", to: "altcha#challenge"
1819

1920
scope "login" do
2021
resource :two_factor_challenge, only: %i[new create], path: "verify"

0 commit comments

Comments
 (0)