Skip to content

Commit

Permalink
add cloudflare turnstyle to login page (#775)
Browse files Browse the repository at this point in the history
* add turnstyle to login page

* format

* add docs

* add tests to login page

* update language
  • Loading branch information
shapiromatron authored Mar 2, 2023
1 parent afc3ecf commit 92b3c32
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 1 deletion.
18 changes: 18 additions & 0 deletions docs/docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,21 @@ Fields include:
- `ALLOW_RIS_IMPORTS`: If true, RIS imports are available when working on assessments. If false, the button is not there, but it's still possible for users to import via RIS if the can find the correct URL. Defaults to true.
- `ANONYMOUS_ACCOUNT_CREATION`: If true, anonymous users can create accounts. If false, only staff can create new accounts via the admin. Defaults to true.
- `THIS_IS_AN_EXAMPLE`: Does nothing; used to test configuration.

### Application monitoring

Application performance and monitoring can be optionally enabled using [Sentry](https://sentry.io/). To enable, add these two environment variables:

- `HAWC_SENTRY_DSN` - site key, used for sentry data ingestion
- `HAWC_SENTRY_SETTINGS`: JSON string of settings to pass to [client](https://docs.sentry.io/platforms/python/guides/django/configuration/options/), for example: `{"traces_sample_rate": 0.1, "send_default_pii": false}`

By default, sentry integration is disabled.

### Human verification

To prevent bots from attempting to login to the server, you can optionally enable [Turnstyle](https://www.cloudflare.com/products/turnstile/). To enable, set these two additional environment variables:

- `TURNSTYLE_SITE`: site key, this is used by the client to interact (public)
- `TURNSTYLE_KEY`: secret key, used by the server to verify the response (private)

By default. human verification is disabled.
5 changes: 5 additions & 0 deletions hawc/apps/common/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .token import TokenBackend

__all__ = [
"TokenBackend",
]
File renamed without changes.
35 changes: 35 additions & 0 deletions hawc/apps/common/auth/turnstyle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Optional

import pydantic
import requests
from django.conf import settings


class SiteVerifyRequest(pydantic.BaseModel):
secret: str
response: str
remoteip: Optional[str]


class SiteVerifyResponse(pydantic.BaseModel):
success: bool
challenge_ts: Optional[str]
hostname: Optional[str]
error_codes: list[str] = pydantic.Field(alias="error-codes", default_factory=list)
action: Optional[str]
cdata: Optional[str]


def validate(turnstile_response: str, user_ip: Optional[str] = None) -> SiteVerifyResponse:
url = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
model = SiteVerifyRequest(
secret=settings.TURNSTYLE_KEY, response=turnstile_response, remoteip=user_ip
)
resp = requests.post(url, data=model.dict())
if resp.status_code != 200:
model = SiteVerifyResponse(success=False, hostname=None)
model.error_codes.extend(
[f"Failure status code: {resp.status_code}", f"Failure details: {resp.text}"]
)
return model
return SiteVerifyResponse(**resp.json())
16 changes: 15 additions & 1 deletion hawc/apps/myuser/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from ...constants import AuthProvider
from ..assessment.autocomplete import AssessmentAutocomplete
from ..common.auth.turnstyle import validate
from ..common.autocomplete import AutocompleteMultipleChoiceField
from ..common.forms import BaseFormHelper
from ..common.helper import url_query
Expand Down Expand Up @@ -271,10 +272,16 @@ class HAWCAuthenticationForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
self.next_url = kwargs.pop("next_url")
super().__init__(*args, **kwargs)
self.enable_turnstyle = len(settings.TURNSTYLE_SITE) > 0

def get_extra_text(self) -> str:
challenge = (
f'<div class="cf-turnstile mb-3" data-sitekey="{settings.TURNSTYLE_SITE}"></div>'
if self.enable_turnstyle
else ""
)
text = f"""<a role="button" class="btn btn-light" href="{reverse("home")}">Cancel</a>
<br/><br/>
<br/><br/>{challenge}
<a href="{reverse("user:reset_password")}">Forgot your password?</a>"""
if AuthProvider.external in settings.AUTH_PROVIDERS:
url = reverse("user:external_auth")
Expand Down Expand Up @@ -315,6 +322,13 @@ def clean(self):
)
elif not self.user_cache.is_active:
raise forms.ValidationError(self.error_messages["inactive"])

if settings.TURNSTYLE_SITE:
token = self.data.get("cf-turnstile-response", "")
response = validate(token)
if not response.success:
raise forms.ValidationError("Failed bot challenge - are you human?")

return self.cleaned_data


Expand Down
2 changes: 2 additions & 0 deletions hawc/main/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@
SESSION_COOKIE_NAME = os.getenv("HAWC_SESSION_COOKIE_NAME", "sessionid")
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
SESSION_CACHE_ALIAS = "default"
TURNSTYLE_SITE = os.environ.get("TURNSTYLE_SITE", "")
TURNSTYLE_KEY = os.environ.get("TURNSTYLE_KEY", "")
INCLUDE_ADMIN = bool(os.environ.get("HAWC_INCLUDE_ADMIN", "True") == "True")

# Server URL settings
Expand Down
3 changes: 3 additions & 0 deletions hawc/templates/registration/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
{% endblock content %}

{% block extrajs %}
{% if form.enable_turnstyle %}
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
{% endif %}
<script type="text/javascript">
document.getElementById('id_username').focus();
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
interactions:
- request:
body: secret=secret&response=
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Content-Length:
- '23'
Content-Type:
- application/x-www-form-urlencoded
User-Agent:
- python-requests/2.27.1
method: POST
uri: https://challenges.cloudflare.com/turnstile/v0/siteverify
response:
body:
string: !!binary |
H4sIAAAAAAAAAxzKywnAIBQEwF72rA3YSvAguhEhfngbTyG9BzLneaCdMyWEM12iA82m+TwLhXCg
N6mN6ttY+/ZGrTlERIdOKdV/xfcDAAD//wMA9OTj90gAAAA=
headers:
CF-RAY:
- 79fe1c52d9a1b06f-ATL
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Mon, 27 Feb 2023 04:17:24 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
Vary:
- Accept-Encoding
alt-svc:
- h3=":443"; ma=86400, h3-29=":443"; ma=86400
status:
code: 200
message: OK
version: 1
25 changes: 25 additions & 0 deletions tests/hawc/apps/myuser/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,31 @@ def test_create_account_link(self):

settings.HAWC_FEATURES.ANONYMOUS_ACCOUNT_CREATION = True

@pytest.mark.vcr
def test_turnstyle(self, settings):
url = reverse("user:login")
success = {"username": "pm@hawcproject.org", "password": "pw"}

# no turnstyle by default
c = Client()
resp = c.get(url)
assert b"challenges.cloudflare.com/turnstile" not in resp.content
assert b'data-sitekey="https://test-me.org"' not in resp.content
resp = c.post(url, data=success)
assert resp.status_code == 302
assert resp.url == "/portal/"

# turnstyle if enabled
c = Client()
settings.TURNSTYLE_SITE = "https://test-me.org"
settings.TURNSTYLE_KEY = "secret"
resp = c.get(url)
assert b"challenges.cloudflare.com/turnstile" in resp.content
assert b'data-sitekey="https://test-me.org"' in resp.content
resp = c.post(url, data=success)
assert resp.status_code == 200
assert resp.context["form"].errors == {"__all__": ["Failed bot challenge - are you human?"]}


class ExternalAuthSetup(ExternalAuth):
# mock user metdata handler for test case
Expand Down

0 comments on commit 92b3c32

Please sign in to comment.