getgrav / getgrav/grav-plugin-api

Captcha support for the Admin2 login form

Open
#29 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.