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 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions proposed/captcha-meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
CAPTCHA Meta Document
=====================

## 1. Summary

The `CaptchaVerifierInterface` defines a standard, implementation-agnostic way to verify whether a user is human or automated, using vendor-provided challenge-response systems. It enables applications and frameworks to substitute CAPTCHA vendors transparently and with minimal effort, especially in crisis scenarios such as API bans, forced migrations, or vendor lock-in.

The interfaces are intentionally minimal: implementors MAY extend for vendor-specific data, but consumers MUST depend only on functionality guaranteed by the PSR, allowing maximum interoperability and rapid reaction to a changing security landscape, while they MAY depend on functionality, provided by specific implementations, but only if they know for sure this functionality exists (via instanceof/typehinting).

## 2. Why Bother?

There are currently several major CAPTCHA providers (such as Google reCAPTCHA, hCaptcha, Yandex SmartCaptcha, Cloudflare Turnstile, and others), as well as growing open-source and self-hosted alternatives. Many PHP libraries and frameworks ship with their own interfaces or concrete adapters for these services, leading to fragmentation and poor interoperability between codebases.

When a sudden vendor policy change, API discontinuation, cost escalation, or regulatory issue occurs, projects are forced to urgently change CAPTCHA vendors. In practice this results in application-wide refactoring, duplicated logic, vendor-specific workarounds, and the risk of business downtime or security lapses due to a breaking change in a critical security layer.

By providing a PSR for CAPTCHA, we enable PHP applications and frameworks to depend on a single interface, making it trivial to swap underlying implementations with minimal code change — often only at the DI configuration or service wiring level. This dramatically reduces migration risk and business impact during vendor crisis situations, promotes interoperability, and lessens the chance of vendor lock-in.

Pros:
* A universal, vendor-agnostic interface for CAPTCHA verification and response;
* Rapid provider swap in emergency situations (API shutdowns, region bans, cost changes);
* Less duplicated code and easier maintenance of libraries and frameworks;
* Consistent exception handling and response format for all vendors;
* Promotes extensible, flexible code design.

Cons:
* Slight abstraction overhead compared to using a single provider’s SDK directly;
* Vendor-specific features that are not part of the interface may require interface extension or custom code;

## 3. Design Decisions

### 3.1 Response Interface vs Boolean

During code-review a question have been asked: `why CaptchaVerifierInterface::verify() returns CaptchaResponseInterface rather than a simple bool`. While the fundamental purpose of a CAPTCHA is a binary distinction (human vs bot), in practice, most modern CAPTCHA providers expose rich additional data, such as "score" for confidence (see: Google reCAPTCHA v3, hCaptcha), messages, challenge metadata, or error diagnostics.

By requiring an interface that supplies at minimum an `isSuccess()`: bool method, this PSR permits codebases to enjoy both vendor-agnostic consumption and the option to access extra data by typehinting implementation extensions. This enables:

* Access to provider-specific scoring/attributes without PSR changes.
* Future-proofing as vendors add richer response objects.
* Compatibility with dependency injection and type-centric application design.
* Shallow-to-deep migration (codebases MAY initially only call `isSuccess()`, later adapt to leverage e.g. `getScore()` if desired).
* Returning only a boolean would foreclose all extensibility.

### 3.2 Exception Interface

Instead of prescribing a concrete exception class, this PSR defines a CaptchaException interface, explicitly extending \RuntimeException. This follows the precedent set by PSR-18 and others, allowing vendor libraries to inject their own exception type hierarchies under a unified ancestor.

### 3.3 Scoring

The increasing adoption of "scoring" instead of simple pass/fail, particularly for invisible-style CAPTCHAs, required that the PSR not constrain implementations to simply returning booleans. Implementations MAY extend the CaptchaResponseInterface to expose a score or provider raw data, but code depending strictly on the PSR MAY always use the `isSuccess()` boolean.

This pattern supports both simple and advanced applications (where scoring thresholds might be required by regulators or business rules) and ensures implementations MAY expose the full richness of their backend APIs while remaining PSR-compliant.

### 3.4 Zero-Refactor Provider Swap

The overarching design goal is to allow replacing any \Psr\Captcha\CaptchaVerifierInterface implementation (e.g. Google, hCaptcha, Yandex, Cloudflare Turnstile, etc) via configuration or dependency injection, without touching application code. This minimizes risk in migration and critical incident response, addressing:

* sudden API or ToS changes by vendors,
* vendor-bans or unexpected region lockouts,
* cost spikes,
* rapid requirement changes (e.g. accessibility mandates).

### 3.5 Error Isolation

CaptchaException MUST be used for errors external to the user; e.g. misconfiguration, lost connectivity to the vendor, failed API authentication, or response parsing errors.
That exception MUST NOT be thrown if CAPTCHA token was actually validated, no matter the result - thus, unsuccessful validation (CAPTCHA provider said that user is a bot) MUST NOT throw an exception as this is not an exceptional case, instead `CaptchaVerifierInterface::isSuccess()` MUST return false
This ensures frontend code able to clearly distinguish between "user failed the challenge" and "site is misconfigured/problematic".
User errors (wrong, missing, or expired CAPTCHA tokens) are always indicated via a negative result from isSuccess().

## 4. People

### 4.1 Editor(s)

* [Ilya Saligzhanov](https://github.com/LeTraceurSnork)

### 4.2 Sponsor(s)

* _Vacant_
124 changes: 124 additions & 0 deletions proposed/captcha.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
CaptchaVerifierInterface for various Captcha services
=====================================================

This document describes common interfaces to query data. These data can be from different sources, from in-memory data to databases, as well as filesystem files.

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be
interpreted as described in [RFC 2119][].

The final implementations MAY decorate the objects with more
functionality than the one proposed but they MUST implement the indicated
interfaces/functionality first.

[RFC 2119]: http://tools.ietf.org/html/rfc2119

## 1. Specification

### 1.1 Definitions

* **CAPTCHA** - A Completely Automated Public Turing test to tell Computers and Humans Apart. A challenge-response test designed to distinguish human users from automated bots.
* **Provider** - A service that implements CAPTCHA functionality by:
* Generating challenges to distinguish humans from automated systems
* Validating user responses to these challenges
* Assessing interaction risk levels
* **Client Token** - A string value generated by the **Provider**'s client-side implementation, representing a single challenge attempt.
* MAY have **Provider**-specific format and expiration
* **Verification** - The process of validating a **Client Token** against the **Provider**'s service. MUST be performed server-side. MAY include client IP validation if required by **Provider**. MAY involve additional risk analysis (e.g., behavioral **SCORING**)
* **Successful Verification** - A determination that:
* The **Client Token** is valid and unexpired
* The challenge was completed satisfactorily
* (When applicable) **SCORING** meets implementation thresholds
* **Failed Verification** Occurs when:
* The token is invalid/expired
* The challenge response was incorrect
* The risk score indicates automated behavior
* Network/configuration errors prevent validation
* **SCORING** - An OPTIONAL provider-specific value indicating confidence in the human/bot determination:
* SHOULD be represented as float between 0.0 (likely bot) and 1.0 (likely human).
* MAY be represented as vice-versa (0.0 = no risk, 1.0 = maximum risk).
* MAY, but SHOULD NOT be represented in other than float formats, e.g. 0 - 100 (as percentages)
* SHOULD be accessible through CaptchaResponseInterface extensions
* Thresholds for success/failure are implementation-defined
* Threshold SHOULD be configurable via CaptchaVerifierInterface extensions

### 1.2 Goal
This specification establishes a standardized interface for CAPTCHA implementations in PHP with the primary objective of **enabling immediate, low-effort substitution of CAPTCHA providers during critical vendor lock-in scenarios**. Specifically addressing real-world crises where:
- Providers discontinue services or change pricing models abruptly
- Security vulnerabilities require emergency provider migration
- Sudden service bans

The interface shall achieve:
1. **Zero-Refactor Replacement**: Allow switching providers (e.g., Google reCAPTCHA -> hCaptcha -> Cloudflare Turnstile) through configuration/vendor changes only
2. **DI Container Readiness**: Enable dependency injection of any compliant implementation without call-site modifications
3. **Vendor Crisis Resilience**: Mitigate business continuity risks during:
- Access restrictions
- Provider API shutdowns
- Compliance requirement changes
4. **Cost Containment**: Eliminate:
- System-wide code refactoring during migrations
- Parallel implementation maintenance
- Provider-specific testing overhead

## 2. Interfaces

### 2.1 CaptchaVerifierInterface

```php
namespace Psr\Captcha;

/**
* Interface of Captcha verification service itself.
* MUST decide whether user passed the Captcha or not and return corresponding response.
* 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 CaptchaVerifierInterface
{
/**
* Verifies client token and decides whether verification was successful or not (is user a bot or not).
*
* @param string $token
*
* @throws CaptchaException if Captcha cannot be validated because of non-user problems (e.g. due to network problems, incorrect secret token, etc.)
* @return CaptchaResponseInterface
*/
public function verify(string $token): CaptchaResponseInterface;
}
```

### 2.2 CaptchaResponseInterface

```php
namespace Psr\Captcha;

/**
* Interface of the object that CaptchaVerifierInterface MUST return on ::verify() method.
* MUST contain enough information to consistently say whether user successfully passed Captcha or not.
* 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
{
/**
* MUST return true/false depends on whether verification was successful or not (is user a bot or not).
*
* @return bool
*/
public function isSuccess(): bool;
}
```

### 2.3 CaptchaException

```php
namespace Psr\Captcha;

/**
* MUST be thrown from CaptchaVerifierInterface methods if Captcha test itself cannot be passed due to any reason that is not user-related - network problems, incorrect secret token, unable to parse request-response, etc.
* MUST NOT be thrown if CAPTCHA was actually performed validation - even if it failed - instead CaptchaVerifierInterface MUST return CaptchaResponseInterface which ::isSuccess() method MUST return false
*/
class CaptchaException extends \RuntimeException
{
}
```