getgrav / getgrav/grav-plugin-api
Captcha support for the Admin2 login form
- Dominant language
- PHP
- Stars
- 5
- Forks
- 8
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 2
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.
Contributor guide
No contributing guide indexed for this repository
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.
Assessment
- Tech stack
- javascript, php
- Domain
- api, authentication, frontend, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100