Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Captcha support for the Admin2 login form

Open
#29 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
48/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
javascript, php

Research direction

Start at AuthController::token(), especially requireFields() and enforceLoginRateLimit(), then compare the /auth/sso/providers discovery flow and Form's CapProvider::getCap(). Trace how admin-next loads SSO providers and sends the login request. Done means discovery, challenge/redeem, verification, configuration, and the Admin2 client flow work for the listed authentication paths without relying on the Form plugin.

Written by the indexing model from the issue text.

Description

Neither the API plugin nor the Login plugin has any captcha support today, so there is no way to put a challenge in front of the Admin2 login form. The Form plugin ships four providers (cap, Turnstile, reCAPTCHA, basic) but they only gate Form-plugin forms — nothing in the login path can reach them. Reported in getgrav/grav#4254.

Where it goes

Admin2 authenticates through POST /api/v1/auth/token → AuthController::token(). That is the single choke point for password login; SSO logins converge later at finalizeAuthenticatedUser(), so a check placed right after requireFields() / enforceLoginRateLimit() covers passwords without disturbing the SSO path.

The OAuth2 provider plumbing is the model to copy: GET /auth/sso/providers fires onApiLoginProviders, and the login page fetches it on mount and renders whatever came back. Everything under /auth/ is already in the router's public prefixes, so a discovery endpoint costs nothing.

This can't be done as a third-party plugin. LDAP works by subscribing to onUserLoginAuthenticate, but AuthController passes only ['username' => …, 'password' => …] into $login->login(), so a captcha token in the request body never reaches an event subscriber.

Plan

  1. GET /api/v1/auth/captcha — public by prefix, returns {enabled, provider, mode, endpoint, site_key}. Generic so Turnstile/reCAPTCHA slot in via site_key; cap needs no key.
  2. POST /api/v1/auth/captcha/challenge and /auth/captcha/redeem — cap.js-wire-compatible, served by the API plugin itself.
  3. Verification in token() before $login->login(); failure throws ValidationException. Same treatment for /auth/forgot-password (a better bot target than login, since it sends mail) and /auth/setup.
  4. Config under plugins.api.login.captcha.* plus an Admin2 settings toggle.
  5. admin-next: one more $effect alongside the SSO one, and the token passed as an extra field on the login request.

Design note: don't proxy to the Form plugin

The obvious shortcut is to point admin-next at the Form plugin's existing /forms-cap/challenge and /forms-cap/redeem. Two problems: it hard-depends on the Form plugin being installed and enabled, and those endpoints emit no CORS headers — fine when admin-next is served under /admin same-origin, broken for a standalone SPA pointed at a remote serverUrl.

Instead the API plugin requires trilbymedia/cap-php directly (a Cap over Psr16Storage($grav['cache']->getSimpleCache()), ~30 lines mirroring CapProvider::getCap()) and serves its own endpoints under /auth/captcha/. Admin login captcha then works on a bare grav + api + admin2 install — no keys, no third-party service, and it inherits the API's CORS and rate limiting. An optional branch delegates to \Grav\Plugin\Form\Captcha\CaptchaFactory when the Form plugin is present, so a site with Turnstile keys already configured gets those for free.

Client side, bundle @cap.js/widget into admin-next rather than script-injecting the Form plugin's vendored cap.min.js, for the same reason. The wasm needs window.CAP_CUSTOM_WASM_URL pointed at a bundled copy or it reaches for jsDelivr.

Notes

  • Admin login is already rate-limited via the Login plugin's checkLoginRateLimit(), and 2FA is supported per-account. Captcha is defense in depth on top of those, not a replacement.
  • Discovery should report enabled: false rather than serve a broken widget when the provider can't run.
Dominant language
PHP
Stars
5
Forks
10
Avg merge
8h 29m
Merged PRs (30d)
6

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from getgrav/grav-plugin-api

All issues in getgrav/grav-plugin-api

Similar issues

More PHP issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.