Skip to content

New PSR for standartizing 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 4 commits into
base: master
Choose a base branch
from

Conversation

LeTraceurSnork
Copy link

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

TTL:
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"

@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.

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?

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.

7 participants