From b237239a1f2fdbfb402ba5fe6f2d9c86dad59db1 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Sun, 1 Dec 2024 19:42:25 +0100 Subject: [PATCH 1/4] Define an endpoint to retrieve a LibreCaptcha captcha --- Gemfile | 1 + Gemfile.lock | 7 +++++++ app/controllers/captcha_controller.rb | 11 +++++++++++ app/services/libre_captcha.rb | 12 ++++++++++++ config/routes.rb | 3 +++ docker-compose.yml | 6 ++++++ nginx.conf | 5 +++++ spec/requests/captcha_spec.rb | 22 ++++++++++++++++++++++ 8 files changed, 67 insertions(+) create mode 100644 app/controllers/captcha_controller.rb create mode 100644 app/services/libre_captcha.rb create mode 100644 spec/requests/captcha_spec.rb diff --git a/Gemfile b/Gemfile index ead69aa..cb91e69 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ gem 'rack-cors' gem 'react-rails' gem 'rubytree' gem 'acts_as_tenant' +gem 'httparty' group :development, :test do gem 'annotaterb' diff --git a/Gemfile.lock b/Gemfile.lock index 7e795fc..c0660de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,6 +126,10 @@ GEM raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) + httparty (0.22.0) + csv + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) i18n (1.14.6) concurrent-ruby (~> 1.0) importmap-rails (2.0.3) @@ -188,6 +192,8 @@ GEM money (6.19.0) i18n (>= 0.6.4, <= 2) msgpack (1.7.2) + multi_xml (0.7.1) + bigdecimal (~> 3.1) net-imap (0.5.1) date net-protocol @@ -398,6 +404,7 @@ DEPENDENCIES devise (~> 4.9) factory_bot_rails faker + httparty importmap-rails jbuilder jsonapi-rails diff --git a/app/controllers/captcha_controller.rb b/app/controllers/captcha_controller.rb new file mode 100644 index 0000000..79f84ea --- /dev/null +++ b/app/controllers/captcha_controller.rb @@ -0,0 +1,11 @@ +class CaptchaController < ApplicationController + skip_before_action :authenticate_user! + + def create + id = LibreCaptcha.new.get_id + render json: { + id:, + media_url: media_captcha_index_url(id:) + }, status: :created + end +end diff --git a/app/services/libre_captcha.rb b/app/services/libre_captcha.rb new file mode 100644 index 0000000..3d49e5a --- /dev/null +++ b/app/services/libre_captcha.rb @@ -0,0 +1,12 @@ +class LibreCaptcha + def get_id + HTTParty.post("http://libre-captcha:8888/v2/captcha", + body: { + input_type: "text", + level: :hard, + media: 'image/png', + size: '350x100' + }.to_json + ).then { |raw| JSON.parse(raw)['id'] } + end +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 10810fe..1357d9c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,6 +23,9 @@ Rails.application.routes.draw do resources :tables_arrangements, only: %i[index show] end + resources :captcha, only: :create do + get 'v2/media', to: 'captcha#media', on: :collection, as: :media + end mount Rswag::Ui::Engine => '/api-docs' mount Rswag::Api::Engine => '/api-docs' diff --git a/docker-compose.yml b/docker-compose.yml index e1a5977..18050d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,12 @@ services: - backend volumes: - ../wedding-planner-frontend/:/app + libre-captcha: + image: librecaptcha/lc-core:latest + volumes: + - "./tmp/libre-captcha-data:/lc-core/data" + ports: + - "8888:8888" nginx: image: nginx:latest ports: diff --git a/nginx.conf b/nginx.conf index 06d059b..6371bdd 100644 --- a/nginx.conf +++ b/nginx.conf @@ -12,6 +12,11 @@ server { proxy_set_header Host $http_host; } + location /captcha/ { + proxy_pass http://libre-captcha:8888/; + proxy_set_header Host $http_host; + } + location / { proxy_pass http://frontend:3000; proxy_set_header Host $http_host; diff --git a/spec/requests/captcha_spec.rb b/spec/requests/captcha_spec.rb new file mode 100644 index 0000000..adac1a8 --- /dev/null +++ b/spec/requests/captcha_spec.rb @@ -0,0 +1,22 @@ +require 'swagger_helper' + +RSpec.describe 'captcha', type: :request do + path '/captcha' do + + post('create a CAPTCHA challenge') do + tags 'CAPTCHA' + consumes 'application/json' + produces 'application/json' + + response(201, 'created') do + schema type: :object, + required: %i[id], + properties: { + id: { type: :string, format: :uuid } + media_url: { type: :string, format: :uri } + } + xit + end + end + end +end From be9ca9e6b01e95d6875a334ff04f69f128ed8ac5 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Sun, 1 Dec 2024 18:43:28 +0000 Subject: [PATCH 2/4] Add copyright notice --- app/controllers/captcha_controller.rb | 2 ++ app/services/libre_captcha.rb | 2 ++ spec/requests/captcha_spec.rb | 2 ++ 3 files changed, 6 insertions(+) diff --git a/app/controllers/captcha_controller.rb b/app/controllers/captcha_controller.rb index 79f84ea..428328d 100644 --- a/app/controllers/captcha_controller.rb +++ b/app/controllers/captcha_controller.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + class CaptchaController < ApplicationController skip_before_action :authenticate_user! diff --git a/app/services/libre_captcha.rb b/app/services/libre_captcha.rb index 3d49e5a..d811437 100644 --- a/app/services/libre_captcha.rb +++ b/app/services/libre_captcha.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + class LibreCaptcha def get_id HTTParty.post("http://libre-captcha:8888/v2/captcha", diff --git a/spec/requests/captcha_spec.rb b/spec/requests/captcha_spec.rb index adac1a8..4cde63a 100644 --- a/spec/requests/captcha_spec.rb +++ b/spec/requests/captcha_spec.rb @@ -1,3 +1,5 @@ +# Copyright (C) 2024 Manuel Bustillo + require 'swagger_helper' RSpec.describe 'captcha', type: :request do From 5f01741943d77907683fa8ec97ee3e348836ace2 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Sun, 1 Dec 2024 19:56:49 +0100 Subject: [PATCH 3/4] Validate the Captcha challenge for account signup --- app/controllers/application_controller.rb | 12 ++++++++++++ app/controllers/users/registrations_controller.rb | 2 ++ app/services/libre_captcha.rb | 6 ++++++ spec/requests/captcha_spec.rb | 4 ++-- spec/requests/schemas.rb | 11 +++++++++++ spec/requests/users/registrations_spec.rb | 3 ++- 6 files changed, 35 insertions(+), 3 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f163c3e..15a4b17 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -30,6 +30,18 @@ class ApplicationController < ActionController::Base private + def validate_captcha! + Rails.logger.info("Captcha params: #{captcha_params}") + + return if LibreCaptcha.new.valid?(id: captcha_params[:id], answer: captcha_params[:answer]) + + render json: { error: 'Incorrect CAPTCHA solution' }, status: :unprocessable_entity + end + + def captcha_params + params.expect(captcha: [:id, :answer]) + end + def default_url_options(options = {}) options.merge(path_params: { slug: ActsAsTenant.current_tenant&.slug }) end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 24d8eca..4894d4b 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -4,6 +4,8 @@ class Users::RegistrationsController < Devise::RegistrationsController clear_respond_to respond_to :json + before_action :validate_captcha!, only: :create + def create wedding = Wedding.create(wedding_params) unless wedding.persisted? diff --git a/app/services/libre_captcha.rb b/app/services/libre_captcha.rb index d811437..8684e9f 100644 --- a/app/services/libre_captcha.rb +++ b/app/services/libre_captcha.rb @@ -11,4 +11,10 @@ class LibreCaptcha }.to_json ).then { |raw| JSON.parse(raw)['id'] } end + + def valid?(id:, answer:) + HTTParty.post("http://libre-captcha:8888/v2/answer", + body: { id:, answer: }.to_json + ).then { |raw| JSON.parse(raw)['result'] == 'True' } + end end \ No newline at end of file diff --git a/spec/requests/captcha_spec.rb b/spec/requests/captcha_spec.rb index 4cde63a..607a2d9 100644 --- a/spec/requests/captcha_spec.rb +++ b/spec/requests/captcha_spec.rb @@ -14,8 +14,8 @@ RSpec.describe 'captcha', type: :request do schema type: :object, required: %i[id], properties: { - id: { type: :string, format: :uuid } - media_url: { type: :string, format: :uri } + id: { type: :string, format: :uuid }, + media_url: { type: :string, format: :uri }, } xit end diff --git a/spec/requests/schemas.rb b/spec/requests/schemas.rb index 6602db8..e1f7b43 100644 --- a/spec/requests/schemas.rb +++ b/spec/requests/schemas.rb @@ -18,5 +18,16 @@ module Swagger example: :default, description: 'Wedding slug' } + + CAPTCHA = { + captcha: { + type: :object, + required: %i[id answer], + properties: { + id: { type: :string, format: :uuid }, + answer: { type: :string } + } + } + } end end \ No newline at end of file diff --git a/spec/requests/users/registrations_spec.rb b/spec/requests/users/registrations_spec.rb index ae0f86d..f29b387 100644 --- a/spec/requests/users/registrations_spec.rb +++ b/spec/requests/users/registrations_spec.rb @@ -30,7 +30,8 @@ RSpec.describe 'users/registrations', type: :request do properties: { date: { type: :string, format: :date}, } - } + }, + **Swagger::Schema::CAPTCHA } } From 71046b9a1c6a4ddb8fa91e0cd1c6c8e948d74248 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Sun, 1 Dec 2024 20:01:00 +0100 Subject: [PATCH 4/4] Avoid exposing internal port and unnecessary endpoints --- docker-compose.yml | 2 +- nginx.conf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 18050d1..64b54c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,7 +41,7 @@ services: volumes: - "./tmp/libre-captcha-data:/lc-core/data" ports: - - "8888:8888" + - 8888 nginx: image: nginx:latest ports: diff --git a/nginx.conf b/nginx.conf index 6371bdd..58c2bb9 100644 --- a/nginx.conf +++ b/nginx.conf @@ -12,8 +12,8 @@ server { proxy_set_header Host $http_host; } - location /captcha/ { - proxy_pass http://libre-captcha:8888/; + location /captcha/v2/media/ { + proxy_pass http://libre-captcha:8888/v2/media/; proxy_set_header Host $http_host; }