Skip to content

Commit

Permalink
Add support for Cloudflare Turnstile
Browse files Browse the repository at this point in the history
Cloudflare Turnstile is an alternative to Recaptcha that avoids user interaction.
More information: https://developers.cloudflare.com/turnstile/
  • Loading branch information
AaronDewes committed Apr 26, 2024
1 parent d1a9a56 commit 3d459b3
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 5 deletions.
32 changes: 30 additions & 2 deletions modules/recaptcha/app/controllers/recaptcha/request_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "recaptcha"
require "net/http"

module ::Recaptcha
class RequestController < ApplicationController
Expand Down Expand Up @@ -28,13 +29,15 @@ class RequestController < ApplicationController
def perform
if OpenProject::Recaptcha::Configuration.use_hcaptcha?
use_content_security_policy_named_append(:hcaptcha)
else
elsif OpenProject::Recaptcha::Configuration.use_turnstile?
use_content_security_policy_named_append(:turnstile)
elsif OpenProject::Recaptcha::Configuration.use_recaptcha?
use_content_security_policy_named_append(:recaptcha)
end
end

def verify
if valid_recaptcha?
if valid_turnstile? || valid_recaptcha?
save_recaptcha_verification_success!
complete_stage_redirect
else
Expand Down Expand Up @@ -70,6 +73,8 @@ def recaptcha_version
2
when ::OpenProject::Recaptcha::TYPE_V3
3
when ::OpenProject::Recaptcha::TYPE_TURNSTILE
99 # Turnstile is not comparable/compatible with recaptcha
end
end

Expand All @@ -84,6 +89,29 @@ def valid_recaptcha?
verify_recaptcha call_args
end

##
#
def valid_turnstile?
return false unless OpenProject::Recaptcha::Configuration.use_turnstile?
token = params["turnstile-response"]
return false if token.blank?

data = {
"response" => token,
"remoteip" => request.remote_ip,
"secret" => recaptcha_settings["secret_key"],
}

data_encoded = URI.encode_www_form(data)

response = Net::HTTP.post_form(
URI("https://challenges.cloudflare.com/turnstile/v0/siteverify"),
data
)
response = JSON.parse(response.body)
response["success"]
end

##
# fail the recaptcha
def fail_recaptcha(msg)
Expand Down
4 changes: 3 additions & 1 deletion modules/recaptcha/app/helpers/recaptcha_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ def recaptcha_available_options
[I18n.t("recaptcha.settings.type_disabled"), ::OpenProject::Recaptcha::TYPE_DISABLED],
[I18n.t("recaptcha.settings.type_v2"), ::OpenProject::Recaptcha::TYPE_V2],
[I18n.t("recaptcha.settings.type_v3"), ::OpenProject::Recaptcha::TYPE_V3],
[I18n.t("recaptcha.settings.type_hcaptcha"), ::OpenProject::Recaptcha::TYPE_HCAPTCHA]
[I18n.t("recaptcha.settings.type_hcaptcha"), ::OpenProject::Recaptcha::TYPE_HCAPTCHA],
[I18n.t("captcha.settings.type_turnstile"), ::OpenProject::Recaptcha::TYPE_TURNSTILE]

]
end

Expand Down
3 changes: 2 additions & 1 deletion modules/recaptcha/app/views/recaptcha/admin/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
<div class="form--field-instructions">
<%= I18n.t('recaptcha.settings.recaptcha_description_html',
hcaptcha_link: link_to('https://docs.hcaptcha.com/switch/', 'https://docs.hcaptcha.com/switch/', target: '_blan'),
recaptcha_link: link_to('https://www.google.com/recaptcha', 'https://www.google.com/recaptcha', target: '_blank')).html_safe %>
recaptcha_link: link_to('https://www.google.com/recaptcha', 'https://www.google.com/recaptcha', target: '_blank'),
turnstile_link: link_to('https://developers.cloudflare.com/turnstile/', 'https://developers.cloudflare.com/turnstile/', target: '_blank')).html_safe %>
</div>
</div>
<div class="form--field">
Expand Down
21 changes: 21 additions & 0 deletions modules/recaptcha/app/views/recaptcha/request/perform.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@
document.getElementById('submit_captcha').submit();
}
<% end %>
<% elsif recaptcha_settings['captcha_type'] == ::OpenProject::Recaptcha::TYPE_TURNSTILE %>
<% input_name = "turnstile-response" %>
<input type="hidden" name="<%= input_name %>" />
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback" defer></script>

<div id="turnstile-container"></div>
<%= nonced_javascript_tag do %>
function submitTurnstileForm(token) {
var input = document.getElementsByName('<%= input_name %>')[0];

input.value = token;
document.getElementById('submit_captcha').submit();
}

window.onloadTurnstileCallback = function () {
turnstile.render('#turnstile-container', {
sitekey: '<%= recaptcha_settings['website_key'] %>',
callback: submitTurnstileForm,
});
};
<% end %>
<% end %>
<% end %>
</div>
6 changes: 5 additions & 1 deletion modules/recaptcha/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ en:
verify_account: "Verify your account"
error_captcha: "Your account could not be verified. Please contact an administrator."
settings:
website_key: 'Website key'
website_key: 'Website key (May also be called "Site key")'
response_limit: 'Response limit for HCaptcha'
response_limit_text: 'The maximum number of characters to treat the HCaptcha response as valid.'
website_key_text: 'Enter the website key you created on the reCAPTCHA admin console for this domain.'
Expand All @@ -20,6 +20,7 @@ en:
type_v2: 'reCAPTCHA v2'
type_v3: 'reCAPTCHA v3'
type_hcaptcha: 'HCaptcha'
type_turnstile: 'Cloudflare Turnstile™'
recaptcha_description_html: >
reCAPTCHA is a free service by Google that can be enabled for your OpenProject instance.
If enabled, a captcha form will be rendered upon login for all users that have not verified a captcha yet.
Expand All @@ -29,3 +30,6 @@ en:
<br/>
HCaptcha is a Google-free alternative that you can use if you do not want to use reCAPTCHA.
See this link for more information: %{hcaptcha_link}
<br/>
Cloudflare Turnstile™ is another alternative that is more convenient for users while still providing the same level of security.
See this link for more information: %{turnstile_link}
1 change: 1 addition & 0 deletions modules/recaptcha/lib/open_project/recaptcha.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Recaptcha
TYPE_V2 ||= "v2"
TYPE_V3 ||= "v3"
TYPE_HCAPTCHA ||= "hcaptcha"
TYPE_TURNSTILE ||= "turnstile"

require "open_project/recaptcha/engine"
require "open_project/recaptcha/configuration"
Expand Down
8 changes: 8 additions & 0 deletions modules/recaptcha/lib/open_project/recaptcha/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ def use_hcaptcha?
type == ::OpenProject::Recaptcha::TYPE_HCAPTCHA
end

def use_turnstile?
type == ::OpenProject::Recaptcha::TYPE_TURNSTILE
end

def use_recaptcha?
type == ::OpenProject::Recaptcha::TYPE_RECAPTCHA_V2 || type == ::OpenProject::Recaptcha::TYPE_RECAPTCHA_V3
end

def type
::Setting.plugin_openproject_recaptcha["recaptcha_type"]
end
Expand Down
7 changes: 7 additions & 0 deletions modules/recaptcha/lib/open_project/recaptcha/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ class Engine < ::Rails::Engine
keys.index_with value
end

SecureHeaders::Configuration.named_append(:turnstile) do
value = %w(https://challenges.cloudflare.com)
keys = %i(frame_src script_src style_src connect_src)

keys.index_with value
end

OpenProject::Authentication::Stage.register(
:recaptcha,
nil,
Expand Down

0 comments on commit 3d459b3

Please sign in to comment.