Captcha support for the Admin2 login form
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
- Domain
- api, authentication, frontend, security
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
GET /api/v1/auth/captcha— public by prefix, returns{enabled, provider, mode, endpoint, site_key}. Generic so Turnstile/reCAPTCHA slot in viasite_key; cap needs no key.POST /api/v1/auth/captcha/challengeand/auth/captcha/redeem— cap.js-wire-compatible, served by the API plugin itself.- Verification in
token()before$login->login(); failure throwsValidationException. Same treatment for/auth/forgot-password(a better bot target than login, since it sends mail) and/auth/setup. - Config under
plugins.api.login.captcha.*plus an Admin2 settings toggle. - admin-next: one more
$effectalongside 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: falserather 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from getgrav/grav-plugin-api
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
getgrav/grav-plugin-api#60 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
getgrav/grav-plugin-api#51 · 2 comments ·
-
Difficulty 5/5 Over a week Newbie friendliness 45/100
getgrav/grav-plugin-api#42 · 1 reaction ·
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
getgrav/grav-plugin-api#40 · 4 comments ·
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
getgrav/grav-plugin-api#32 · 1 comment ·
All issues in getgrav/grav-plugin-api
Similar issues
-
major-release
Difficulty 1/5 Under an hour Newbie friendliness 67/100
Intervention/image#1553 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
omegaup/omegaup#10353 · 1 comment ·
Maintainers usually reply within 5 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 61/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
effort: low graphiql ide impact: low scope: code quality type: chore
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
wp-graphql/wp-graphql#4486 ·
Maintainers usually reply within 1 day