Skip to content

New PSR for standardizing CAPTCHA (CaptchaInterface) #1330

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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

LeTraceurSnork
Copy link

@LeTraceurSnork LeTraceurSnork commented Jun 11, 2025

Here's a proposal of new PSR for standartizing CAPTCHAs to one interface
Continuation of this thread: https://discord.com/channels/788815898948665366/788816084383694848/1379332512886689812

TL;DR:
Recently we've faced with a problem that government forbids to gather data abroad and we urgently need to switch Google ReCaptcha to other solutions, but the thing is that since those solutions doesn't have common interface yet, the codebase needs to be refactored, which isn't good - the task in a nutshell is just "switch vendors"

UPD1: demo-repository was created https://github.com/LeTraceurSnork/psr-captcha-verifier
UPD2: Working Group can communicate now via Discord-channel
(https://discord.gg/uAfJDfY2)
UPD3: Clarification of an interface purpose and my POV #1330 (comment)
UPD4: finally got my hands on Captchas SDK library and managed to implement suggested interface in it
https://github.com/LeTraceurSnorkLibrary/SDKaptcha
Feel free to see what I mean by this proposal on practice

@LeTraceurSnork LeTraceurSnork requested a review from a team as a code owner June 11, 2025 16:20
@jaapio
Copy link
Member

jaapio commented Jun 11, 2025

I do not really see why we need a psr for this. An interface can always be used by any developer in any application.

The problem of recaptcha is a very specific implementation to validate humans interacting with your website. While there are other solutions.

Can you explain why you think this should be a standard?

@LeTraceurSnork
Copy link
Author

@jaapio

Can you explain why you think this should be a standard?
I'll try in examples

So, for example, I have a Google ReCaptcha on my website, lets say I installed it via composer from corresponding repository. Let's omit frontend integration and look at the backend, we have smth like this:

class FormSubmitService
{
    private $recaptcha;

    public function __construct(\ReCaptcha\ReCaptcha $recaptcha)
    {
        $this->recaptcha = $recaptcha;
    }

    public function submitForm(CustomRequest $request): CustomResponse
    {
        $token = $request->get('g-recaptcha-response');
        
        $response = $this->recaptcha->verify($token);
                                    
        if (!$response->isSuccess()) {
            return new CustomResponse('Recaptcha failed!');
        }
        
        // ... continue to submit fom
    }
}

//...

$recaptcha = new \ReCaptcha\ReCaptcha('my_secret_token');
$formSubmitService = new FormSubmitService($recaptcha);
$formSubmitService->submitForm($incomeCustomRequest);

So, what happens if we at some moment switch to another Captcha vendor (another SDK)? We immediately lose the \ReCaptcha\ReCaptcha class, __construct() fails with Type error, DI ruined. At this point we want to rely on some interface like this:

public function __construct(CaptchaInterface $captcha)
{
    $this->captcha= $captcha;
}

because in this scenario we can pass:

$recaptcha = new \ReCaptcha\ReCaptcha('my_secret_token');
$hcaptcha = new \HCaptcha\HCaptcha('my_secret_token');
$mcaptcha= new \MCaptcha\MCaptcha('my_secret_token');
$cloudflareTurnstile= new \Cloudflare\Turnstile('my_secret_token');
$smartCaptcha= new \Yandex\SmartCaptcha('my_secret_token');
$formSubmitService = new FormSubmitService($recaptcha);
$formSubmitService2 = new FormSubmitService($hcaptcha);
$formSubmitService3 = new FormSubmitService($mcaptcha);
$formSubmitService4 = new FormSubmitService($cloudflareTurnstile);
$formSubmitService5 = new FormSubmitService($smartCaptcha);

Yes, we will need an additional configuration when constructing the Captcha object itself, but inside the form handler there will be no changes, 'cause all of the above will be able to say true/false $response->isSuccess(), so the switching between providers will be much easier

@KorvinSzanto
Copy link
Contributor

I'm still on the fence about whether this deserves a PSR or not, I think that's going to depend on what kind of buy-in we can get from the projects that we'd expect to benefit from this.

There are some additional things that come with the request that captcha services typically either require or optionally allow like the visitors IP address. It'd be better in my opinion to depend on PSR-7 and pass the entire RequestInterface implementation to the verify method so that the implementation can decide what to gather.
Probably also would benefit from exceptions for the different known exceptional cases like a PSR-6-esque InvalidArgumentException and maybe a ValidationRequestErrorException or similar so that folks can adequately handle exceptions without needing to know what underlying tools are being used by the implementation.

@KorvinSzanto
Copy link
Contributor

Whoops, fat fingered the close button.

@LeTraceurSnork
Copy link
Author

@KorvinSzanto since all of the most popular Captchas (and, I guess, all of the external Captchas) requires secret token to connect to their validation routes, I guess it would benefit to add InvalidArgumentException to CaptchaInterface::verify() to handle incorrect secret token cases.
Not quite sure about ValidationRequestErrorException tho, but I guess smth like that can be added to CaptchaInterface::verify() as well, but I'm not quite sure to what scenario. Captcha service did not respond? Respond with not 2** (404, 500, etc.)? Respond with 200 but "Captcha not passed"? 🤔

@mbeccati
Copy link

Have you already gathered some consensus between the developers of the major captcha libraries in PHP?

@KorvinSzanto
Copy link
Contributor

@KorvinSzanto since all of the most popular Captchas (and, I guess, all of the external Captchas) requires secret token to connect to their validation routes, I guess it would benefit to add InvalidArgumentException to CaptchaInterface::verify() to handle incorrect secret token cases. Not quite sure about ValidationRequestErrorException tho, but I guess smth like that can be added to CaptchaInterface::verify() as well, but I'm not quite sure to what scenario. Captcha service did not respond? Respond with not 2** (404, 500, etc.)? Respond with 200 but "Captcha not passed"? 🤔

The exceptions would be used for exceptional cases that represent neither pass nor fail, for example:

  • The provided request to validate includes multiple captcha tokens, or no token at all
  • The implementation sends an http request that fails
  • The implementation gets a response that can't be parsed

Without defined exceptions, all of these cases would throw implementation-specific exceptions like a guzzle exception or a recaptcha sdk exception and would require the consumer to know the implementation to properly catch exceptions.

So an implementation would do something like:

try {
    $response = $guzzle->send($validationRequest);
    $isValid = $this->validateResponse($response);
} catch (GuzzleException $e) {
    throw new ImplementationSpecificValidationRequestErrorException('Failed to send validation request', $e);
} catch (ValidationResponseException $e) {
    throw new ImplementationSpecificValidationRequestErrorException('Invalid validation response', $e);
}

and a consumer can avoid knowing that guzzle is in use at all and do:

try {
    $isValid = $captcha->validate($whatever);
} catch (\Psr\Captcha\ValidationRequestErrorException) {
    // Show user "We're having trouble validating your request, please try again in a few minutes"
} catch (\Psr\Captcha\InvalidArgumentException) {
    // Show user "Invalid request, please try again"
}

if (!$isValid) {
    // Show user "Invalid captcha, please try again"
}

@LeTraceurSnork
Copy link
Author

LeTraceurSnork commented Jun 11, 2025

Have you already gathered some consensus between the developers of the major captcha libraries in PHP?

@mbeccati
Unfortunately, I did not, 'cause neither Google, hCaptcha, Yandex, nor their core developers in person did not respond to an invitation 😕
I will continue my attempts tho, now it is more convenient with a PR opened

@KorvinSzanto CaptchaException added, seems reasonable. Feel free to open review threads on those interfaces please 🙂

@garak
Copy link
Member

garak commented Jun 12, 2025

Sadly, the Google recaptcha PHP repo is dead.
An issue for PHP 8.4 deprecation has been open for months, with no feedback. The last commit dates back to February 2023.

Copy link
Member

@samdark samdark left a comment

Choose a reason for hiding this comment

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

Overall, I like the idea. The problem domain, indeed, is pretty clear so could be standardized.

captcha.md Outdated
* SHOULD contain actual user's SCORING
* MAY contain additional information (e.g., gathered from it's captcha-vendor service's verification endpoint) (i.e. message, errors, etc.)
*/
interface CaptchaResponseInterface
Copy link
Member

Choose a reason for hiding this comment

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

Why is it an interface and not a bool?

Copy link
Author

Choose a reason for hiding this comment

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

You mean, why CaptchaInterface::verify() returns that instead of a bool? If yes, then answer is - 'cause you might want to implement additional methods to specific CaptchaResponse classes, like getScore(), getHost() (for instance, those fields implemented by Google ReCaptcha, hCaptcha, SmartCaptcha) or similar - to extend response data

Copy link
Member

Choose a reason for hiding this comment

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

Should be mentioned in meta-document.

Copy link
Author

Choose a reason for hiding this comment

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

mentioned it in captcha-meta.md (added meta-document)

captcha.md Outdated
* SHOULD contain method to configure SCORING threshold (if applicable by PROVIDER)
* SHOULD throw a CaptchaException as soon as possible if appears any non-user related error that prevents correct Captcha solving (e.g. network problems, incorrect secret token, e.g.)
*/
interface CaptchaInterface
Copy link
Member

Choose a reason for hiding this comment

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

I think there might be a need for challenge(string $token): string method which, given a token, generates the challenge for the user to solve.

Copy link
Author

Choose a reason for hiding this comment

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

Idk what did you mean by this, probably CaptchaInterface needs to be renamed to CaptchaVerifierInterface, 'cause that is its purpose - to verify that passed token is valid. But idk what else can be retrieved from $token, especially in string. Do you mean challenge is frontend rendered I am not a robot checkbox with traffic lights? If so, i'm not sure that service should interact with it - it's just a verifier

Copy link
Member

Choose a reason for hiding this comment

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

Well, for example, given a token that is 24, challenge might be 20+4 or something like that.

Copy link
Author

Choose a reason for hiding this comment

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

Well, that interface is not quite for that purpose, but rather to verify already solved captcha task by its token using some external (or even internal, it doesnt actually matter) captcha service. Roughly, it's an interface to build SDK on

Copy link
Member

Choose a reason for hiding this comment

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

I see. Makes sense.

@samdark
Copy link
Member

samdark commented Jun 12, 2025

Here's Yii2 implementation: https://github.com/yiisoft/yii2/tree/master/framework/captcha

@LeTraceurSnork
Copy link
Author

LeTraceurSnork commented Jun 18, 2025

Here is Shopware 6 implementation https://github.com/shopware/shopware/tree/e4c3f565857f8e6c53d3c8e35f6b00dfd0eb3f73/src/Storefront/Framework/Captcha and Shopware 5 implementation https://github.com/shopware5/shopware/tree/8779bb0fc2cff04bb92dbba8ea5522263a71ab48/engine/Shopware/Components/Captcha

Do you believe that Shopware could use and achieve profit from implementation of this interfaces by recieving more information from responses?

@LeTraceurSnork
Copy link
Author

Added demo-repository
https://github.com/LeTraceurSnork/psr-captcha-verifier

@phpfui
Copy link

phpfui commented Jun 19, 2025

I am the maintainer of a PHP 8.4 compatible version of Google's Recaptcha with about 115K downloads so far and growing.

I think this is an excellent idea for standardization. I would be happy implement any PSR that is agreed to in my fork. But be aware that Google has announced that they will be turning off this version of the service at the end of 2025.

That said, my goal is to allow an easy upgrade path to the newer Google versions of their API with the current package, hopefully with no or minor changes needed by the developer.

captcha.md Outdated

## 2. Interfaces

### 2.1 CaptchaInterface
Copy link

Choose a reason for hiding this comment

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

This line should now read:

### 2.1 CaptchaVerifierInterface

Due to the code change.

Copy link
Author

Choose a reason for hiding this comment

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

done

@VincentLanglet
Copy link

VincentLanglet commented Jun 26, 2025

I feel like Captcha feature is something too specific to be worth a whole personal PSR.
And looking at the interface makes me believe this more.

When looking at:

public function verify(string $token): CaptchaResponseInterface;

I feel like this is not the right signature cause you might need more informations to verify something

Google accepts the IP
If I rely on your interface but call $catch->verify($token, $ip) the benefit is null since I cannot be sure that when I'll change the lib I'll use, the signature will be the same. Using a signature verifiy(string $token, array $context) could help but same, it cannot guarantee the array keys will be the same.

Shopware lib use the request
This does not seems compatible with your interface, and asking the user to parse the request in order to get the token doesn't seems right to me since you're coupling the implementations.

CaptchaResponseInterface is not better, even if the phpdoc is saying the implementation might have extra methods, if I want to be able to switch implementation easily in my project, I have to use only the one provided by the interface. So no score, no error message, and so on...

I believe we'll end with something like

use Psr\Captcha\RequestInterface;
use Psr\Captcha\ResponseInterface;

interface CaptchaInterface
{
    /**
     * @param RequestInterface $request
     *
     * @return ResponseInterface
     *
     * @throws \Psr\Captcha\ExceptionInterface If an error happens while processing the request.
     */
    public function verify(RequestInterface $request): ResponseInterface;
}

when the responseInterface could have

  • Score ?float
  • isSuccess bool
  • and so on

But it seems really close to the PSR 7

@LeTraceurSnork
Copy link
Author

I feel like this is not the right signature cause you might need more informations to verify something

@VincentLanglet
Yes, and you can set those information with additional ->set methods or even with constructor params. The main point of this interface is that you can receive this verifier as DI in, say, constructor, and this verifier object is already pumped with all data it needs. Something like:

// Imagine that we're on REST API route controller
$captcha = (new CaptchaVerifier('secret_token', $maybe_http_client))
    ->setIp($maybe_ip)
    ->setTime($maybe_time)
    ->setHost($maybe_host);

$formHandler = new AbstractFormSubmitHandler($captcha);
$formHandler->submit(['token' => $userToken, ...]);

And inside of it we will need to verify this token via specified Captcha, but we do not know what this Captcha is and we do not need to know that, 'cause AbstractFormSubmitHandler::__construct() looks like that:

class AbstractFormSubmitHandler
{
    function __construct(CaptchaVerifierInterface $captcha_verifier){...}
}

And all we do - is just using already pumped object as a Validator, you can say. Token was passed to ::submit(), $captcha_verifier->verify($token) and that's all. This AbstractFormSubmitHandler does not need to be rewritten if Captcha provider is changed, 'cause it does not depend on it.
This approach allows AbstractFormSubmitHandler to remain decoupled from specific captcha implementations, relying only on the interface.

On the other hand, I do understand you concerns about mutable $captcha object, but as I see:

  1. Theoretically speaking, it can be immutable (e.g., 100500 constructor params, array $data as param, etc.)
  2. All of those params (ip, host, time, smth else) are unnecessary (not required), at least on those Captcha providers that I discovered. You can pass them in addition, but Capctha will pass without them as well
  3. This interfaces does not restrict you from using them, because verify() is abstract and setters is omitted from interface

@LeTraceurSnork
Copy link
Author

CaptchaResponseInterface is not better, even if the phpdoc is saying the implementation might have extra methods, if I want to be able to switch implementation easily in my project, I have to use only the one provided by the interface. So no score, no error message, and so on

@VincentLanglet on the second part of your message
Thank you for raising an important issue with CaptchaResponseInterface being too minimalistic.

While I believe that having just isSuccess() covers the most basic use case — telling whether the captcha has been passed or not, and keeps the code simple and swappable.
However, as you pointed out, some providers such as ReCaptcha v3 return extra details like 'score', error messages, etc. With the current contract, this data becomes inaccessible if using DI-bound code, unless adding hacks like downcasting or specific type checks, which are not ideal.
A few ways to extend:

  • Add a generic getData(): array to expose arbitrary provider details; drawbacks: brittle, no typing. But that's just not good
  • Let verify() return a more specific response interface for each implementation, but then consumer code is tied to implementation, so we made a circle
  • Include optional getters in the base interface (getScore(), getErrorCodes()), but for many implementations these would need to return null or throw.

For now, the base interface solves the simple validation problem well, but for advanced response handling, it's worth discussing which extensibility pattern (if any) would be preferable — whether as a universal getData(), or a more formal structure.

Thanks for surfacing this; now we're talking and I believe this is what WG should discuss and eventually solve

@phpfui
Copy link

phpfui commented Jun 27, 2025

I am going to go with what @LeTraceurSnork said. The whole point is to provide a minimal interface that all Capthca systems have to provide. It is minimal, but with some proper code around it, you can easily and quickly swap in another provider. With PHP, that could be configured with a ini file or database entry. The code will work the same. You can then fine tune the new provider if needed.

And as for minimal interfaces, I find the current Container interface also to minimal to be usefull, but that was agreed upon and in use. So I don't think this interface should be rejected simply because it does not account for all use cases. It accounts for enough to be useful and will help standardize Captcha libraries to make them more generic and easy to swap out if needed.

@Crell
Copy link
Contributor

Crell commented Jun 27, 2025

The de facto pattern of several past PSRs has been that the interfaces provide the "read" but not "configure" API. Configure is left up to the particular implementation, and you configure it in advance. The API is just for plugging it in and using it, assuming most of the configuration is already handled before the service gets injected.

(PSR-3 Logging, PSR-11 Containers, PSR-14 Events, PSR-20 Clocks, etc. all take that approach.)

@VincentLanglet
Copy link

The last question I have in mind is "Which feature should have a PSR" and which shouldn't.

If we have a CaptchaInterface should we have some

  • AuthenticatorInterface
  • MailerInterface
  • TranslatorInterface
  • FeatureToggleInterface
  • ...

which are also often used feature ?

Where will be the limit ? (Maybe the answer is none)

@KorvinSzanto
Copy link
Contributor

KorvinSzanto commented Jun 27, 2025

It's important to keep in mind that not all captchas are built the same. They won't always have a single value to validate, though I concede most do today.

The de facto pattern of several past PSRs has been that the interfaces provide the "read" but not "configure" API. Configure is left up to the particular implementation, and you configure it in advance. The API is just for plugging it in and using it, assuming most of the configuration is already handled before the service gets injected.

To me, captcha input is closer to a cache item than a simple log message. Like the event PSR, we should at the very least accept an object instead of a string. As it sits now, one must know the implementation to successfully use a provided CaptchaInterface and that doesn't jive with the likes of PSR-(3, 11, 14, 20) which can work entirely without knowing the implementation.

In my opinion it'd be better to accept a full PSR-7 request in validate, or in a factory that builds the value to validate. That way I can pass a captcha interface to my controllers and validate the request without needing to cater each controller to properly extract the required information from the request.

@phpfui
Copy link

phpfui commented Jun 27, 2025

The future of software is interoperable standards that allow things to be mixed and matched and not have vendor lock in. Vendors want lock in, standards prevent that. So yes, we need more standard interfaces, mail and translation would be two good ones to tackle.

We have standards in most industries. Metric bolts for example. JIS (Japanese Industrial Standard) comes to mind. The more we insist on standard interfaces the better off as a PHP community we are in the long run.

@LeTraceurSnork
Copy link
Author

The last question I have in mind is "Which feature should have a PSR" and which shouldn't.

@VincentLanglet as I confessed to @samdark recently, I'm an inclusionist, as wikipedians would say 🙂 In my believe, all of your mentioned interfaces may actually be accepted if written properly, reviewed, agreed upon, etc. So, to Where will be the limit ? I would say 'the answer is none'. More PSRs to PSR god 🤩
p.s. isn't TranslatorInterface suggested in PSR-21? 🤔

It's important to keep in mind that not all captchas are built the same. They won't always have a single value to validate, though I concede most do today

@KorvinSzanto yes, absolute most of the Captchas operates token as a string. I did some researches, I've got two different sources that representeed different information about CAPTCHA providers usage percentage, so I've checked following: Google ReCaptcha, Cloudflare Turnstile, hCaptcha, Friendly Captcha, AWS WAF, Yandex SmartCaptcha, GeeTest, Tencent Captcha, Yidun Captcha, MTCaptcha, Altcha, VAPTCHA and KeyCAPTCHA. From all of those (that's more than 99,9% of market) only Altcha verifies something that is not a string - their SDK method accepts array of some data, but even that case is totally fits in presented interface - we with @VincentLanglet discussed it earlier - it can be achieved via mutable objects with ->setters(). Now, after almost all of the known market being analyzed, I guess, even if there is some captcha provider that verifies, say, two tokens simultaneously (and both are required, so we can't go with ->setters()) we can just agree that this one is just too exotic for an interface

To me, captcha input is closer to a cache item than a simple log message. Like the event PSR, we should at the very least accept an object instead of a string. As it sits now, one must know the implementation to successfully use a provided CaptchaInterface

Oh, I see your point. Yes, while we could not have an opportunity to pump the verifier with additional data it needs beforehand (e.g. Verifier needs some data from HttpRequest that being validated right now and the one that actually contains the token), I guess, it is wise to give it an opportunity to do so. But what do you think of an idea to keep the verify($token) as it is, but to add withData(object $data) or withData(array $data) method (or smth like that)?

In my opinion it'd be better to accept a full PSR-7 request in validate

With this being said, I guess, the question "what do we do with Response then" is much more acute for now

@phpfui totally agree with you! More standarts -> less exotic packages that doesn't fits them -> less odd unsupportable legacy

@andrew-demb
Copy link
Contributor

I invite everyone interested in participating in the development/support (nominally, we need a working group) to join via the link: https://discord.com/channels/788815898948665366/1386802002599739443

How to join?
This is not an invite link, but just a channel link

@LeTraceurSnork
Copy link
Author

I invite everyone interested in participating in the development/support (nominally, we need a working group) to join via the link: https://discord.com/channels/788815898948665366/1386802002599739443

How to join?
This is not an invite link, but just a channel link

Pardon, link changed to https://discord.gg/uAfJDfY2

@andrew-demb
Copy link
Contributor

In my opinion, making the captcha validator strict to use PSR-7 request will make usage with simple captcha (which won't need the request) providers too complex.

What about having two arguments:

  • captcha token
  • context object (with empty interface CaptchaContextInterface)

And to provide additional interface - CaptchaRequestAwareInterface (with method getRequest(): RequestInterface).

This will allow:

  • making dependency captcha to PSR-7 to be optional
  • consumers are not required to provide a PSR-7 request if they are using simple captcha providers
  • providers that rely on the HTTP request to require in the runtime to use CaptchaRequestAwareInterface context, or to fallback to default logic in case of limited context

@LeTraceurSnork
Copy link
Author

I'm quite sceptical about PSR-7 interface usage at all - because even if its optional - how do we parse it without knowing the implementation? E.g.:

class CaptchaVerifier implements CaptchaVerifierInterface
  function withData(RequestInterface $psr7_request)
  {
    $this->psr7_request = $psr7_request;
    return $this;
  }

  function validate(string $token)
  {
    $http_client  = new HttpClient(); // in this example PSR-18 compatible

    $request_contents = $this->psr7_request->getBody()->getContents();
    $request_data = json_decode($request_contents);
    // maybe some other manipulations
    // @var array<string, mixed> $request_data

    // <=== START
    $http_request = new HttpRequest([ // PSR-7 compatible
      'token' => $token,
      'ip' => $request_data['ip'],
      'host' => $request_data['host'],
      'some_value' => $request_data['some_value'],
    ]);
    // <=== END
    $http_client->sendRequest();
  }
}

(new CaptchaVerifier())->withData($psr7_request)->verify($token);

It's not perfect example, but it shows the problem - between START and END how do we know for sure that fields 'ip', 'host', 'some_value' exists AND have those names? Why not 'client_ip', 'site_host', 'some_other_value'? We cannot rely on them because we don't know the implementation, only the interface. That's why more proper (but, I guess, not ideal as well) appoach would be:

class CaptchaVerifier implements CaptchaVerifierInterface
  function withData(array $data)
  {
    $this->ip = $data['ip'] ?? null;
    $this->host = $data['host'] ?? null;
    $this->some_value = $data['some_value'] ?? null;
    return $this;
  }

  function validate(string $token)
  {
    $http_client  = new HttpClient(); // in this example PSR-18 compatible
    $request_data = [
      'token' => $token,
      'ip' => $ip,
      'host' => $host,
      'some_value' => $some_value,
    ];
    // maybe some other manipulations
    // @var array<string, mixed> $request_data

    $http_request = new HttpRequest($request_data);  // PSR-7 compatible
    $http_client->sendRequest($http_request);
  }
}

(new CaptchaVerifier())->withData($psr7_request)->verify($token);

That alternative also would omit CaptchaRequestAwareInterface as unnecesssary

@andrew-demb
Copy link
Contributor

@LeTraceurSnork

  1. Let's not consider any mutating methods aka "setRequest"/"withRequest" to verifiers.

All data should be directly passed to the method


  1. I don't think it will be necessary to parse request content. Most cases is about:
  • IP (server request attributes, server request server params, reading headers)
  • host (RequestInterface::getUri() - UriInterface::getHost())
<?php

// verifier that requires token, IP, host and some other data (optional)
class AcmeCaptchaVerifier implements CaptchaVerifierInterface
{
    public function __construct(private readonly string $requestIpAttributeName)
    {
    }

    public function validate(string $token, ContextInterface $context)
    {
        $someValue = null;
        if ($context instanceof AcmeContextInterface) {
            $ip = $context->getIpForAcmeProvider();
            $host = $context->getHostForAcmeProvider();
            $someValue = $context->getSomeValueForAcmeProvider();
        } elseif ($context instanceof RequestAwareContextInterface && $context->getRequest() instanceof \Psr\Http\Message\ServerRequestInterface) {
            /** @var \Psr\Http\Message\ServerRequestInterface $request */
            $request = $context->getRequest();
            $ip = $request->getAttribute($this->requestIpAttributeName, $request->getServerParams()['REMOTE_ADDR'] ?? null);
            $host = $request->getUri()->getHost();
            $someValue = null; // unable to determine
        } else {
            // OR fallback to some default values for $ip, $host, $someValue
            throw new \InvalidArgumentException('Unsupported context - should be AcmeContextInterface or RequestAwareContextInterface to determine IP and host');
        }

        $http_client = new HttpClient(); // in this example PSR-18 compatible

        // <=== START
        $http_request = new HttpRequest(
            [ // PSR-7 compatible
                'token' => $token,
                'ip' => $ip,
                'host' => $host,
                'some_value' => $someValue, // some other data
            ],
        );
        // <=== END
        $http_client->sendRequest();
    }
}

// verifier that uses ONLY token
class FooCaptchaVerifier implements CaptchaVerifierInterface
{
    public function __construct(private readonly string $requestIpAttributeName)
    {
    }

    public function validate(string $token, ContextInterface $context)
    {
        $http_client = new HttpClient(); // in this example PSR-18 compatible

        // <=== START
        $http_request = new HttpRequest(
            [ // PSR-7 compatible
                'token' => $token,
            ],
        );
        // <=== END
        $http_client->sendRequest();
    }
}

(new CaptchaVerifier())->verify($token, new AcmeContext('127.0.0.1', 'example.com', 'some_value'));
(new CaptchaVerifier())->verify($token, new Psr7Context(Psr7Request::createFromGlobals()));

@andrew-demb
Copy link
Contributor

... and why don't "withData" or "array $context" - it won't be typed. Very hard to decide unified keys what can be used by different implementations.

My proposal based on "empty interface" at a core and (at least) one additional extending interface that knows about PSR-7 request

@KorvinSzanto KorvinSzanto changed the title New PSR for standartizing CAPTCHA (CaptchaInterface) New PSR for standardizing CAPTCHA (CaptchaInterface) Jun 28, 2025
@LeTraceurSnork
Copy link
Author

I think, I got your point
What if we add the second parameter to CaptchaVerifierInterface::verify(), but it will be optional?
Smth like this:

public function verify(string $token, CaptchaContextInterface = new NoContext()): CaptchaResponseInterface;

And then add smth like this:

interface CaptchaContextInterface
{
// need to think about its content and methods
}

class NoContext implements CaptchaContextInterface
{
// all of its methods will be stubs that do nothing/returns empty data
}

Then we can pass some context to verify() if we need it (creating it beforehand), but if we don't - then just call ->verify($token) and context will be empty

@KorvinSzanto
Copy link
Contributor

But what do you think of an idea to keep the verify($token) as it is, but to add withData(object $data) or withData(array $data) method (or smth like that)?

This is okay if it's immutable, but I think it's unnecessary when the data is required to be passed every time for proper interop.

It's not perfect example, but it shows the problem - between START and END how do we know for sure that fields 'ip', 'host', 'some_value' exists AND have those names? Why not 'client_ip', 'site_host', 'some_other_value'? We cannot rely on them because we don't know the implementation, only the interface.

The IP and the host would come from ->getServerParams but I hear your underlying question.

The name of the query parameter that gets used should be known at the time the verifier is constructed so they can be configured against the verifier instance.

$configuredVerifier = new FooVerifier(captchaKey: 'my_custom_captcha_key');

One issue you didn't bring up that is demonstrated in your example is the request's body isn't parsed in a ServerRequestInterface. Also the body stream might not be seekable, so this spec would need a separate derivative object like:

class CaptchaRequest
{
    public function __construct(
        /** Either a parsed query or a parsed post body */
        public readonly array $parsedRequest,
        public readonly UriInterface $uri,
        public readonly array $serverParams,
    ) {}
}

What if we add the second parameter to CaptchaVerifierInterface::verify(), but it will be optional?

This requires the caller to be aware of the implementation for interop.


To me this is sounding more and more like a part of a validator spec rather than it's own captcha focused thing.

@LeTraceurSnork
Copy link
Author

I don't think it will be necessary to parse request content. Most cases is about:
IP (server request attributes, server request server params, reading headers)
host (RequestInterface::getUri() - UriInterface::getHost())

@andrew-demb My point was that if you rely on anything else rather than a token, you need to either pass it directly to ::verify() method, but that could be a problem as discussed earlier (you don't know the implementation and what fields does it require) OR you need to pass them to CaptchaVerifier class itself during the configuration step - and only after it the verificator can be passed as a param to, say, AbstractFormSubmitHandler constructor. And after it you are no more able to set additional params, because they are behing the interface. So, with that said, I only see two options:

  1. Methods ->withData() anyway (or similar)
  2. Verifier factory

@KorvinSzanto

@LeTraceurSnork
Copy link
Author

LeTraceurSnork commented Jun 28, 2025

p.s. may I ask you to officialy join the Working Group? @KorvinSzanto @andrew-demb @phpfui @VincentLanglet

@LeTraceurSnork
Copy link
Author

Update to clarify my perspective further:
While the thread’s title mentions CAPTCHA—and we all recognize CAPTCHA as "those traffic lights, motorcycles, and fire hydrants in square boxes"—I intended the interface to convey a slightly deeper meaning in the context of this proposal.
Here, I suggest considering CAPTCHA as a verifier that, from a purely backend perspective, receives a token from the user on the frontend, passes it to the backend, and then validates this token in the implementation class to determine whether the user is a bot or not.
Of course, the first idea for obtaining such a token is completing the classic image-based CAPTCHA. However, this is not the only solution. Given that even within the discussions around this PSR, some argue that CAPTCHA is an anti-pattern, I guess we’ll see an increasing variety of alternative approaches in the future.
The core purpose of the interface is to tell these solutions that, from the backend’s standpoint, it doesn’t matter how they operate on the frontend—as long as they pass a string-based token in the request.

@VincentLanglet
Copy link

The core purpose of the interface is to tell these solutions that, from the backend’s standpoint, it doesn’t matter how they operate on the frontend—as long as they pass a string-based token in the request.

Personally I'm not convinced about such incomplete interface since

  • Not all captcha are string-token based
  • Not all catcha are token based only

Asking to call some extra method withIp(), withScore() and so on which are not in the interface lose all the benefit.

What the purpose of writing

class FormService
{
      public function __construct(private readonly CaptchaVerifier $captcha) {}
      
      public function submitForm(CustomRequest $request): CustomResponse
    {
        $token = $request->get('g-recaptcha-response');
        
        $this->recaptacha->withIp($request->getIp()); // THIS BREAKS THE BENEFIT OF THE INTERFACE
        $response = $this->recaptcha->verify($token);
                                    
        if (!$response->isSuccess()) {
            return new CustomResponse('Recaptcha failed!');
        }
    }

and we could even argue it's odd to rely on the key 'g-recaptcha-response' which might change from a CaptchaVerifier to another, passing the request directly feels better.

Anyway...

@LeTraceurSnork
Copy link
Author

@VincentLanglet

Not all captcha are string-token based
Not all catcha are token based only

Well, that may be true, though I've researched that topic - top Captchas by usage percentage - ALL is string-token based. As do, I assume, most of self-hosted Captchas. So, based on that I'm proposing to standartize specifically them, not some other Captchas that may exist and may be based on, idk, file-uploading

Asking to call some extra method withIp(), withScore() and so on which are not in the interface lose all the benefit

Totally agree! That's why I suppose that usage in your example should be more like:

class FormService
{
     public function __construct(private readonly CaptchaVerifier $captcha) {}
      
     public function submitForm(Psr7Request $psr7_request): Psr7Response
     {
        $token = $psr7_request->get('captcha-token'); // something that not based on exact implementation. Though, it may vary - it's on developer's choice
        
        $response = $this->recaptcha->verify($token);
                                    
        if (!$response->isSuccess()) {
            return new Psr7Response('Recaptcha failed!');
    }
}

$captcha = new SomeImplementationCaptcha(); // implements CaptchaVerifierInterface
$captcha->withIp($ip)->withSomethingElse($somethingElse);
$formService = new FormService($captcha);
$formService->submitForm($psr7_request);

and we could even argue it's odd to rely on the key 'g-recaptcha-response' which might change from a CaptchaVerifier

Well, yeah... But, as I mentioned in example above, that's on developer's conscience 🧐

@LeTraceurSnork
Copy link
Author

LeTraceurSnork commented Jul 10, 2025

Gentlemen, all who is interested in this PSR, or at least in discussing it - I believe, we need to at least duplicate this converstaion (maybe partially) to this official discussion in Google Groups: https://groups.google.com/g/php-fig/c/v8VymG3L9no to progress it further

@phphleb
Copy link

phphleb commented Jul 11, 2025

This is a pretty cool initiative and seems like it could be super handy. If you think about the main point of CAPTCHA, it’s to tell humans apart from bots (like a Voight-Kampff test but on the web). The ways people find to bypass CAPTCHAs are always evolving, and because of that, CAPTCHA implementations have to keep getting smarter too. If we limit CAPTCHA’s flexibility through a PSR, we might end up forcing developers to ditch unique, creative protection methods that don’t fit the PSR mold.

Now, tying this to PSR-7 (handling all the request data), as suggested earlier, means that tons of sites that don’t use PSR-7 are going to have a tough time just implementing a regular CAPTCHA. Currently, without a set standard, it’s easier, and users are accustomed to how simple it is. Take WordPress for example; it doesn’t have PSR-7, and neither do lots of other sites; changing a CAPTCHA there is pretty straightforward.

From a corporate perspective, where tasks are split into backend and frontend, a standard might indeed speed things up on the backend, but it won’t really help the frontend guys, which means it won’t speed up CAPTCHA implementation or replacement overall. It might make life easier for backend developers, which is what we're aiming for, right?

Despite this criticism, I think the idea has potential if it’s developed further. Though, I have to admit, I’m not an expert in CAPTCHA technology.

@LeTraceurSnork
Copy link
Author

Now, tying this to PSR-7 (handling all the request data), as suggested earlier, means that tons of sites that don’t use PSR-7 are going to have a tough time just implementing a regular CAPTCHA

Excatly my worries. I've been worried that thousands of developers (maybe even students on their first work) will have struggling with unneeded and overloading functionality. I see that the much interface simpler the more it is easier to understand, hence use, hence it much more popular and widely implemented.

If we limit CAPTCHA’s flexibility through a PSR, we might end up forcing developers to ditch unique, creative protection methods that don’t fit the PSR mold

Few words on that - as I mentioned earlier, I don't really think that by narrowing token format with string we really limiting anything. Today the only conceivable way to pass the Captcha result from frontend to backend (and convenient enough) is string. The other one, like, I can't even imagine - phone call? SMS? Divine entity that split yes and no? Those ways just too exotic 😂 But, possibly, if this interface will really become insufficient - then yes, it might be need an improvement. But I guess we should anyway accept it beforehand, 'cause insufficient interface is better than non-existent one :-)

@LeTraceurSnork
Copy link
Author

UPD4: finally got my hands on Captchas SDK library and managed to implement suggested interface in it
https://github.com/LeTraceurSnorkLibrary/SDKaptcha
Feel free to see what I mean by this proposal on practice

@phpfui
Copy link

phpfui commented Jul 11, 2025

You can also see my implementation for Google's ReCaptcha here: https://github.com/phpfui/recaptcha/tree/PSR

@LeTraceurSnork
Copy link
Author

@phpfui yeah, I had a brief look at it - this is it :-) Not quite as I thought Exception would be handled, but I guess it's just Google's legacy, that you decided to keep backwards compatibility with. They insist that network errors is just another type of Response with different code type instead of explicit Exception that is easier to handle, but anyway, the core concept persists :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.