Skip to content

Commit

Permalink
Merge pull request #15398 from AaronDewes/turnstile
Browse files Browse the repository at this point in the history
feat: Add support for Cloudflare Turnstile
  • Loading branch information
oliverguenther authored Aug 19, 2024
2 parents 953f154 + ca2c31f commit 6dee520
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 5 deletions.
12 changes: 12 additions & 0 deletions docs/system-admin-guide/authentication/recaptcha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,15 @@ You can configure the following options:
4. Press the **Apply** button to save your changes.

![Sysadmin authentication reCAPTCHA](Sys-admin-authentication-recaptcha.png)


# Cloudflare Turnstile configuration

To use Cloudflare Turnstile with OpenProject, you need to configure the reCAPTCHA settings in the Cloudflare dashboard. Please see the following link for more details on Cloudflare Turnstile and how to configure it: [https://developers.cloudflare.com/turnstile/](https://developers.cloudflare.com/turnstile/).

Once you created a sitekey and secret key in the Cloudflare dashboard, you can configure OpenProject to use these keys.

1. Select "Turnstile" in the reCAPTCHA admin settings.
2. Insert the **website key**.
3. Insert the **secret key**.
4. Press the **Apply** button to save your changes.
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 @@ -30,13 +31,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 @@ -72,6 +75,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 @@ -86,6 +91,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("recaptcha.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 @@ -25,7 +25,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: '_blank'),
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['recaptcha_type'] == ::OpenProject::Recaptcha::TYPE_TURNSTILE %>
<% input_name = "turnstile-response" %>
<input type="hidden" name="<%= input_name %>" />
<%= nonced_javascript_include_tag "https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback" %>

<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_V2 || type == ::OpenProject::Recaptcha::TYPE_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 style_src connect_src)

keys.index_with value
end

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

0 comments on commit 6dee520

Please sign in to comment.