Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: reCAPTCHA + HTML5 form validation #1071

Merged
merged 12 commits into from
Oct 24, 2022
Merged

Conversation

thekaveman
Copy link
Member

Fixes #1068

Based on an answer to this SO post: https://stackoverflow.com/q/41665935/453168

What this PR does

Uses programmatic reCAPTCHA validation

This is the main issue being solved. reCAPTCHA v3 supports two options:

Using a programmatic challenge allows the browser's HTML5 form validation a chance to kick-in before reCAPTCHA's checks,
which isn't possible with the challenge button auto-bind.

Refactors how templates get reCAPTCHA data

Before we used a Django context processor, which gave the data to every request context, even those that didn't need it:

def recaptcha(request):
    """Context processor adds recaptcha information to request context."""
    return {
        "recaptcha": {
            "api_url": settings.RECAPTCHA_API_URL,
            "enabled": settings.RECAPTCHA_ENABLED,
            "site_key": settings.RECAPTCHA_SITE_KEY,
        }
    }

This PR introduces a Middleware, following our usual pattern where we can apply the middleware per-view that needs it, which adds the reCAPTCHA data directly to the request:

class RecaptchaEnabled(MiddlewareMixin):
    """Middleware configures the request with required reCAPTCHA settings."""

    def process_request(self, request):
        if settings.RECAPTCHA_ENABLED:
            request.recaptcha = {
                "data_field": recaptcha.DATA_FIELD,
                "script_api": settings.RECAPTCHA_API_KEY_URL,
                "site_key": settings.RECAPTCHA_SITE_KEY,
            }
        return None

# later in views.py

@decorator_from_middleware(RecaptchaEnabled)
def confirm(request):
    """View handler for the eligibility verification form."""
    # etc.

This makes it more explicit which views use reCAPTCHA, and also makes using reCAPTCHA data easy from templates. Like in base.html where we include the reCAPTCHA API library only once and only if needed:

<html>
    <body>
        <!-- etc -->

        {% if request.recaptcha %}<script src="{{ request.recaptcha.script_api }}"></script>{% endif %}
    </body>
</html>

Makes the eligibility:unverified path a redirect

This has a couple advantages:

  • (the reason for this change): creates a new request, clearing reCAPTCHA data from prior /eligiblity/confirm request
  • Mirrors how the eligibility:verified path works (ultimately a redirect to enrollment:index)
  • URL is now /eligiblity/unverified instead of /eligibility/confirm, a more accurate UX

Testing this PR

Setup

Add this to your .vscode/launch.json under the Django: Benefits Client, Debug=False entry's env:

{
  "name": "Django: Benefits Client, Debug=False",
  // other fields...
  "env": {
    // existing fields...
   // add these 2 entries with the values for the test reCAPTCHA site
    "DJANGO_RECAPTCHA_SITE_KEY": "<SITE KEY HERE>",
    "DJANGO_RECAPTCHA_SECRET_KEY": "<SECRET KEY HERE>"
  }
}

Eligibility index

reCAPTCHA library included once at the bottom of <body>:

image

HTML5 validation works, form submits with valid data:

choose-benefit-validation

Eligibility confirm

reCAPTCHA library included once at the bottom of <body>:

image

HTML5 validation works, form submits with valid data:

confirm-validation

Other pages

Every other page should not have the reCAPTCHA library or any reCAPTCHA handling for form elements.

E.g. the homepage does not include reCAPTCHA:

image

Nor does the eligibility:start page:

image

we don't need the recaptcha attributes on this button
allows HTML5 field validation to occur before the recaptcha check

adapted from https://stackoverflow.com/a/63290578/453168
* adds settings directly to request.recaptcha
* middleware instead of context processor --> can be applied per-view
only present if recaptcha enabled for the view with the form
ensure the verifier selection form checks the recaptcha value
@thekaveman thekaveman requested a review from a team as a code owner October 14, 2022 22:04
@github-actions github-actions bot added tests Related to automated testing (unit, UI, integration, etc.) front-end HTML/CSS/JavaScript and Django templates deployment-dev [auto] Changes that will trigger a deploy if merged to dev back-end Django views, sessions, middleware, models, migrations etc. labels Oct 14, 2022
@thekaveman thekaveman added this to the Courtesy Cards milestone Oct 14, 2022
@thekaveman thekaveman self-assigned this Oct 14, 2022
otherwise returning the view function directly uses the
same request object, with the recaptcha data from the original
confirm request

this also causes the URL to change from /eligibility/confirm
to /eligibility/unverified (wasn't shown before, but is a valid route)
@thekaveman thekaveman force-pushed the fix/recaptcha-html5-validation branch from 12737dc to efac987 Compare October 14, 2022 22:16
@machikoyasuda
Copy link
Member

I just wrote these notes for myself to help document what the app is doing for forms and what JS each form has

These are the 4 forms that use the form.html includes on the app and their corresponding IDs:

  • #verifier-selection - has recaptcha
  • #eligibility-verification - has recaptcha, has button name change
  • #card-tokenize-fail - does not have recaptcha
  • #card-tokenize-success - does not have recaptcha

The header's language selector button is also a form, but it does not use form.html.

machikoyasuda
machikoyasuda previously approved these changes Oct 24, 2022
Copy link
Member

@machikoyasuda machikoyasuda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really like the way the JS in form.html is cleaned up! I also really like the instructions/documentation on how to run recaptcha locally - this will be very helpful.

I added the note comment there just to make sure we never make a CSS ID that collides with the ID name of a form. (ie - Ensuring we never make another #verifier-selection ID).

Once this PR is merged, I believe the unverified page should have the proper new layout that is on dev already.

@thekaveman
Copy link
Member Author

I added the note comment there just to make sure we never make a CSS ID that collides with the ID name of a form

That's a good point - we could prefix these new form IDs with form- to make that more clear and less likely for a collision?

angela-tran
angela-tran previously approved these changes Oct 24, 2022
Copy link
Member

@angela-tran angela-tran left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice refactoring!

@machikoyasuda
Copy link
Member

I added the note comment there just to make sure we never make a CSS ID that collides with the ID name of a form

That's a good point - we could prefix these new form IDs with form- to make that more clear and less likely for a collision?

Yeah. This could be done in another PR.

help avoid future CSS selector collision
@thekaveman thekaveman dismissed stale reviews from angela-tran and machikoyasuda via e783ce1 October 24, 2022 20:27
@thekaveman thekaveman merged commit fe247eb into dev Oct 24, 2022
@thekaveman thekaveman deleted the fix/recaptcha-html5-validation branch October 24, 2022 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
back-end Django views, sessions, middleware, models, migrations etc. deployment-dev [auto] Changes that will trigger a deploy if merged to dev front-end HTML/CSS/JavaScript and Django templates tests Related to automated testing (unit, UI, integration, etc.)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

reCAPTCHA is bypassing HTML5 form validation
3 participants