From 812a09e20cf55b11bd4771bb595c4b8ac90eead4 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 28 Jul 2026 14:51:20 +0300 Subject: [PATCH 1/5] Split authentication method interfaces --- CHANGELOG.md | 1 + README.md | 9 +++++++-- src/AuthenticationMethodInterface.php | 25 ++----------------------- src/AuthenticatorInterface.php | 18 ++++++++++++++++++ src/ChallengeInterface.php | 18 ++++++++++++++++++ src/Method/Composite.php | 12 ++++++++---- src/Middleware/Authentication.php | 13 ++++++++----- tests/AuthenticationMiddlewareTest.php | 20 ++++++++++++++++++++ 8 files changed, 82 insertions(+), 34 deletions(-) create mode 100644 src/AuthenticatorInterface.php create mode 100644 src/ChallengeInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b5df46..d7f7e6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Enh #104: Explicitly mark readonly properties (@vjik) - Enh #105: Explicitly import classes and functions in "use" section (@mspirkov) - Enh #107: Remove unnecessary files from Composer package (@mspirkov) +- Enh #113: Split `AuthenticationMethodInterface` into focused authentication and challenge interfaces (@samdark) ## 3.2.1 December 17, 2025 diff --git a/README.md b/README.md index 91c4fb6..457c0d0 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,8 @@ Custom authentication callback set in the above is the same as default behavior ### HTTP bearer authentication -Bearer HTTP authentication is typically used in APIs. Authentication token is passed in `WWW-Authenticate` header. +Bearer HTTP authentication is typically used in APIs. The authentication token is passed in the request's +`Authorization` header. On failure, the failure handler adds a `WWW-Authenticate` response header. ```php $authenticationMethod = new \Yiisoft\Auth\Method\HttpBearer($identityRepository); @@ -131,7 +132,11 @@ $authenticationMethod = new \Yiisoft\Auth\Method\Composite([ Typically, that is `UserIdentity`. - `\Yiisoft\Auth\IdentityWithTokenRepositoryInterface` could be additionally implemented by your application identity repository class in case token-based authentication is needed. Typically, that is `UserIdentity`. -- `\Yiisoft\Auth\AuthenticationMethodInterface` could be implemented to provide your own authentication method. +- `\Yiisoft\Auth\AuthenticatorInterface` should be implemented to provide your own authenticator. +- `\Yiisoft\Auth\ChallengeInterface` could be additionally implemented by an authenticator that needs to modify the + authentication failure response, for example, to add an HTTP authentication challenge. +- `\Yiisoft\Auth\AuthenticationMethodInterface` combines both interfaces and is deprecated. Existing implementations + remain compatible; new implementations should use the focused interfaces. ## Documentation diff --git a/src/AuthenticationMethodInterface.php b/src/AuthenticationMethodInterface.php index dc40212..b2a4cc9 100644 --- a/src/AuthenticationMethodInterface.php +++ b/src/AuthenticationMethodInterface.php @@ -4,30 +4,9 @@ namespace Yiisoft\Auth; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; - /** - * The interface that should be implemented by individual authentication methods. + * @deprecated Implement {@see AuthenticatorInterface} and, if a challenge is needed, {@see ChallengeInterface}. */ -interface AuthenticationMethodInterface +interface AuthenticationMethodInterface extends AuthenticatorInterface, ChallengeInterface { - /** - * Authenticates the identity based on information available from request. - * - * @param ServerRequestInterface $request Request to get identity information from. - * - * @return IdentityInterface|null An instance of identity or null if there is no match. - */ - public function authenticate(ServerRequestInterface $request): ?IdentityInterface; - - /** - * Adds challenge to response upon authentication failure. - * For example, some appropriate HTTP headers may be added. - * - * @param ResponseInterface $response Response to modify. - * - * @return ResponseInterface Modified response. - */ - public function challenge(ResponseInterface $response): ResponseInterface; } diff --git a/src/AuthenticatorInterface.php b/src/AuthenticatorInterface.php new file mode 100644 index 0000000..0e02505 --- /dev/null +++ b/src/AuthenticatorInterface.php @@ -0,0 +1,18 @@ +methods as $method) { - if (!$method instanceof AuthenticationMethodInterface) { - throw new RuntimeException('Authentication method must be an instance of ' . AuthenticationMethodInterface::class . '.'); + if (!$method instanceof AuthenticatorInterface) { + throw new RuntimeException('Authentication method must be an instance of ' . AuthenticatorInterface::class . '.'); } $identity = $method->authenticate($request); @@ -41,7 +43,9 @@ public function authenticate(ServerRequestInterface $request): ?IdentityInterfac public function challenge(ResponseInterface $response): ResponseInterface { foreach ($this->methods as $method) { - $response = $method->challenge($response); + if ($method instanceof ChallengeInterface) { + $response = $method->challenge($response); + } } return $response; } diff --git a/src/Middleware/Authentication.php b/src/Middleware/Authentication.php index 9f70db1..cbe88d7 100644 --- a/src/Middleware/Authentication.php +++ b/src/Middleware/Authentication.php @@ -9,7 +9,8 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Yiisoft\Auth\AuthenticationMethodInterface; +use Yiisoft\Auth\AuthenticatorInterface; +use Yiisoft\Auth\ChallengeInterface; use Yiisoft\Auth\Handler\AuthenticationFailureHandler; use Yiisoft\Strings\WildcardPattern; @@ -35,7 +36,7 @@ final class Authentication implements MiddlewareInterface private array $wildcards = []; public function __construct( - private AuthenticationMethodInterface $authenticationMethod, + private AuthenticatorInterface $authenticationMethod, ResponseFactoryInterface $responseFactory, ?RequestHandlerInterface $authenticationFailureHandler = null, ) { @@ -50,9 +51,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $request = $request->withAttribute(self::class, $identity); if ($identity === null && !$this->isOptional($request)) { - return $this->authenticationMethod->challenge( - $this->failureHandler->handle($request), - ); + $response = $this->failureHandler->handle($request); + + return $this->authenticationMethod instanceof ChallengeInterface + ? $this->authenticationMethod->challenge($response) + : $response; } return $handler->handle($request); diff --git a/tests/AuthenticationMiddlewareTest.php b/tests/AuthenticationMiddlewareTest.php index 51e21b1..192152e 100644 --- a/tests/AuthenticationMiddlewareTest.php +++ b/tests/AuthenticationMiddlewareTest.php @@ -14,6 +14,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Yiisoft\Auth\AuthenticationMethodInterface; +use Yiisoft\Auth\AuthenticatorInterface; use Yiisoft\Auth\IdentityInterface; use Yiisoft\Auth\Middleware\Authentication; use Yiisoft\Http\Status; @@ -150,6 +151,25 @@ public function testCustomAuthenticationFailureResponse(): void $this->assertEquals($failureResponse, (string) $response->getBody()); } + public function testAuthenticatorWithoutChallenge(): void + { + $authenticator = new class implements AuthenticatorInterface { + public function authenticate(ServerRequestInterface $request): ?IdentityInterface + { + return null; + } + }; + $handler = $this->createMock(RequestHandlerInterface::class); + $handler + ->expects($this->never()) + ->method('handle'); + + $response = (new Authentication($authenticator, $this->responseFactory)) + ->process(new ServerRequest('GET', '/'), $handler); + + $this->assertSame(Status::UNAUTHORIZED, $response->getStatusCode()); + } + public function testImmutability(): void { $original = new Authentication( From 03407e58a32e54667e071753a5125870347fbe5e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Jul 2026 12:06:37 +0000 Subject: [PATCH 2/5] Apply PHP CS Fixer and Rector changes (CI) --- src/AuthenticationMethodInterface.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/AuthenticationMethodInterface.php b/src/AuthenticationMethodInterface.php index b2a4cc9..e8bc694 100644 --- a/src/AuthenticationMethodInterface.php +++ b/src/AuthenticationMethodInterface.php @@ -7,6 +7,4 @@ /** * @deprecated Implement {@see AuthenticatorInterface} and, if a challenge is needed, {@see ChallengeInterface}. */ -interface AuthenticationMethodInterface extends AuthenticatorInterface, ChallengeInterface -{ -} +interface AuthenticationMethodInterface extends AuthenticatorInterface, ChallengeInterface {} From a87b49dce2b68d8a7e1aa90c770567420166ec7c Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 28 Jul 2026 19:31:49 +0300 Subject: [PATCH 3/5] Ignore false-positive BC check --- .roave-backward-compatibility-check.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.roave-backward-compatibility-check.xml b/.roave-backward-compatibility-check.xml index b399121..071e9f2 100644 --- a/.roave-backward-compatibility-check.xml +++ b/.roave-backward-compatibility-check.xml @@ -2,5 +2,11 @@ xsi:noNamespaceSchemaLocation="vendor/roave/backward-compatibility-check/Resources/schema.xsd"> #\[BC\] SKIPPED: Roave\\BetterReflection\\Reflection\\ReflectionClass "Yiisoft\\Yii\\Debug# + + ~^\[BC\] CHANGED: The parameter \$authenticationMethod of Yiisoft\\Auth\\Middleware\\Authentication\#__construct\(\) changed from Yiisoft\\Auth\\AuthenticationMethodInterface to a non-contravariant Yiisoft\\Auth\\AuthenticatorInterface$~ From 2953e3ca55f583958001e5467c2611c23ee72b60 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 28 Jul 2026 21:57:31 +0300 Subject: [PATCH 4/5] Fix BC ignore regex delimiter --- .roave-backward-compatibility-check.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.roave-backward-compatibility-check.xml b/.roave-backward-compatibility-check.xml index 071e9f2..f2a3524 100644 --- a/.roave-backward-compatibility-check.xml +++ b/.roave-backward-compatibility-check.xml @@ -7,6 +7,6 @@ the constructor parameter type. Roave compares the two package versions independently and cannot resolve this newly introduced cross-version parent relationship as contravariant. --> - ~^\[BC\] CHANGED: The parameter \$authenticationMethod of Yiisoft\\Auth\\Middleware\\Authentication\#__construct\(\) changed from Yiisoft\\Auth\\AuthenticationMethodInterface to a non-contravariant Yiisoft\\Auth\\AuthenticatorInterface$~ + #^\[BC\] CHANGED: The parameter \$authenticationMethod of Yiisoft\\Auth\\Middleware\\Authentication\#__construct\(\) changed from Yiisoft\\Auth\\AuthenticationMethodInterface to a non-contravariant Yiisoft\\Auth\\AuthenticatorInterface$# From 698eb79efa07fb10601ff4ac5c545d03de0c51f7 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Wed, 5 Aug 2026 16:54:22 +0300 Subject: [PATCH 5/5] Improve interface segregation (#119) --- CHANGELOG.md | 2 +- README.md | 8 +++---- src/AuthenticationMethodInterface.php | 5 +++-- src/AuthenticatorInterface.php | 3 +++ src/AuthenticatorWithChallengeInterface.php | 23 +++++++++++++++++++++ src/ChallengeInterface.php | 18 ---------------- src/Method/Composite.php | 6 +++--- src/Method/HttpBasic.php | 3 ++- src/Method/HttpBearer.php | 3 ++- src/Method/HttpCookie.php | 7 ++++++- src/Method/HttpHeader.php | 7 ++++++- src/Method/QueryParameter.php | 7 ++++++- src/Middleware/Authentication.php | 4 ++-- 13 files changed, 61 insertions(+), 35 deletions(-) create mode 100644 src/AuthenticatorWithChallengeInterface.php delete mode 100644 src/ChallengeInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7111c2e..9bb027c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,12 @@ ## 3.2.2 under development +- New #113: Split `AuthenticationMethodInterface` into focused authentication and challenge interfaces (@samdark, @vjik) - Bug #116: Fix authentication scheme in `HttpBearer` challenge according to RFC 6750 (@samdark) - Chg #104: Bump minimal PHP version to 8.1 (@vjik) - Enh #104: Explicitly mark readonly properties (@vjik) - Enh #105: Explicitly import classes and functions in "use" section (@mspirkov) - Enh #107: Remove unnecessary files from Composer package (@mspirkov) -- Enh #113: Split `AuthenticationMethodInterface` into focused authentication and challenge interfaces (@samdark) ## 3.2.1 December 17, 2025 diff --git a/README.md b/README.md index 457c0d0..46c880a 100644 --- a/README.md +++ b/README.md @@ -133,10 +133,10 @@ $authenticationMethod = new \Yiisoft\Auth\Method\Composite([ - `\Yiisoft\Auth\IdentityWithTokenRepositoryInterface` could be additionally implemented by your application identity repository class in case token-based authentication is needed. Typically, that is `UserIdentity`. - `\Yiisoft\Auth\AuthenticatorInterface` should be implemented to provide your own authenticator. -- `\Yiisoft\Auth\ChallengeInterface` could be additionally implemented by an authenticator that needs to modify the - authentication failure response, for example, to add an HTTP authentication challenge. -- `\Yiisoft\Auth\AuthenticationMethodInterface` combines both interfaces and is deprecated. Existing implementations - remain compatible; new implementations should use the focused interfaces. +- `\Yiisoft\Auth\AuthenticatorWithChallengeInterface` could be implemented instead by an authenticator that also + needs to modify the authentication failure response, for example, to add an HTTP authentication challenge. +- `\Yiisoft\Auth\AuthenticationMethodInterface` is equivalent to `AuthenticatorWithChallengeInterface` and is + deprecated. Existing implementations remain compatible; new implementations should use the focused interfaces. ## Documentation diff --git a/src/AuthenticationMethodInterface.php b/src/AuthenticationMethodInterface.php index e8bc694..adeb27a 100644 --- a/src/AuthenticationMethodInterface.php +++ b/src/AuthenticationMethodInterface.php @@ -5,6 +5,7 @@ namespace Yiisoft\Auth; /** - * @deprecated Implement {@see AuthenticatorInterface} and, if a challenge is needed, {@see ChallengeInterface}. + * @deprecated Implement {@see AuthenticatorInterface} or, if a challenge is needed, + * {@see AuthenticatorWithChallengeInterface}. */ -interface AuthenticationMethodInterface extends AuthenticatorInterface, ChallengeInterface {} +interface AuthenticationMethodInterface extends AuthenticatorWithChallengeInterface {} diff --git a/src/AuthenticatorInterface.php b/src/AuthenticatorInterface.php index 0e02505..24b54b6 100644 --- a/src/AuthenticatorInterface.php +++ b/src/AuthenticatorInterface.php @@ -8,6 +8,9 @@ /** * Authenticates an identity using information available in a request. + * + * Implement this interface only if the authenticator does not need to add a challenge to the response upon + * authentication failure. Otherwise, implement {@see AuthenticatorWithChallengeInterface} instead. */ interface AuthenticatorInterface { diff --git a/src/AuthenticatorWithChallengeInterface.php b/src/AuthenticatorWithChallengeInterface.php new file mode 100644 index 0000000..988107b --- /dev/null +++ b/src/AuthenticatorWithChallengeInterface.php @@ -0,0 +1,23 @@ +methods as $method) { - if ($method instanceof ChallengeInterface) { + if ($method instanceof AuthenticatorWithChallengeInterface) { $response = $method->challenge($response); } } diff --git a/src/Method/HttpBasic.php b/src/Method/HttpBasic.php index 52c000d..7fa06d0 100644 --- a/src/Method/HttpBasic.php +++ b/src/Method/HttpBasic.php @@ -7,6 +7,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Yiisoft\Auth\AuthenticationMethodInterface; +use Yiisoft\Auth\AuthenticatorWithChallengeInterface; use Yiisoft\Auth\IdentityInterface; use Yiisoft\Auth\IdentityWithTokenRepositoryInterface; use Yiisoft\Http\Header; @@ -28,7 +29,7 @@ * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] * ``` */ -final class HttpBasic implements AuthenticationMethodInterface +final class HttpBasic implements AuthenticationMethodInterface, AuthenticatorWithChallengeInterface { private string $realm = 'api'; private ?string $tokenType = null; diff --git a/src/Method/HttpBearer.php b/src/Method/HttpBearer.php index dac3909..6812cc8 100644 --- a/src/Method/HttpBearer.php +++ b/src/Method/HttpBearer.php @@ -5,6 +5,7 @@ namespace Yiisoft\Auth\Method; use Psr\Http\Message\ResponseInterface; +use Yiisoft\Auth\AuthenticatorWithChallengeInterface; use Yiisoft\Http\Header; /** @@ -12,7 +13,7 @@ * * @see https://tools.ietf.org/html/rfc6750 */ -final class HttpBearer extends HttpHeader +final class HttpBearer extends HttpHeader implements AuthenticatorWithChallengeInterface { protected string $headerName = Header::AUTHORIZATION; diff --git a/src/Method/HttpCookie.php b/src/Method/HttpCookie.php index 14d1a9b..494a0b4 100644 --- a/src/Method/HttpCookie.php +++ b/src/Method/HttpCookie.php @@ -7,6 +7,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Yiisoft\Auth\AuthenticationMethodInterface; +use Yiisoft\Auth\AuthenticatorInterface; use Yiisoft\Auth\IdentityInterface; use Yiisoft\Auth\IdentityWithTokenRepositoryInterface; @@ -15,7 +16,7 @@ * * @see https://tools.ietf.org/html/rfc6265 */ -final class HttpCookie implements AuthenticationMethodInterface +final class HttpCookie implements AuthenticationMethodInterface, AuthenticatorInterface { private string $cookieName = 'access-token'; private ?string $tokenType = null; @@ -35,6 +36,10 @@ public function authenticate(ServerRequestInterface $request): ?IdentityInterfac return $this->identityRepository->findIdentityByToken($authToken, $this->tokenType); } + /** + * @deprecated No-op kept only for compatibility with the deprecated {@see AuthenticationMethodInterface}. + * HTTP cookie authentication does not need a challenge. + */ public function challenge(ResponseInterface $response): ResponseInterface { return $response; diff --git a/src/Method/HttpHeader.php b/src/Method/HttpHeader.php index 91f7ea3..8b455fd 100644 --- a/src/Method/HttpHeader.php +++ b/src/Method/HttpHeader.php @@ -8,6 +8,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Yiisoft\Auth\AuthenticationMethodInterface; +use Yiisoft\Auth\AuthenticatorInterface; use Yiisoft\Auth\IdentityInterface; use Yiisoft\Auth\IdentityWithTokenRepositoryInterface; @@ -20,7 +21,7 @@ * {@see IdentityWithTokenRepositoryInterface::findIdentityByToken()} * and passes the value of the `X-Api-Key` header. This implementation is used mainly for authenticating API clients. */ -class HttpHeader implements AuthenticationMethodInterface +class HttpHeader implements AuthenticationMethodInterface, AuthenticatorInterface { protected string $headerName = 'X-Api-Key'; @@ -43,6 +44,10 @@ public function authenticate(ServerRequestInterface $request): ?IdentityInterfac return null; } + /** + * @deprecated No-op kept only for compatibility with the deprecated {@see AuthenticationMethodInterface}. + * HTTP header authentication does not need a challenge. + */ public function challenge(ResponseInterface $response): ResponseInterface { return $response; diff --git a/src/Method/QueryParameter.php b/src/Method/QueryParameter.php index ff543ad..0004cd8 100644 --- a/src/Method/QueryParameter.php +++ b/src/Method/QueryParameter.php @@ -7,6 +7,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Yiisoft\Auth\AuthenticationMethodInterface; +use Yiisoft\Auth\AuthenticatorInterface; use Yiisoft\Auth\IdentityInterface; use Yiisoft\Auth\IdentityWithTokenRepositoryInterface; @@ -15,7 +16,7 @@ /** * QueryParameter supports the authentication based on the access token passed through a query parameter. */ -final class QueryParameter implements AuthenticationMethodInterface +final class QueryParameter implements AuthenticationMethodInterface, AuthenticatorInterface { private string $parameterName = 'access-token'; private ?string $tokenType = null; @@ -32,6 +33,10 @@ public function authenticate(ServerRequestInterface $request): ?IdentityInterfac return null; } + /** + * @deprecated No-op kept only for compatibility with the deprecated {@see AuthenticationMethodInterface}. + * Query parameter authentication does not need a challenge. + */ public function challenge(ResponseInterface $response): ResponseInterface { return $response; diff --git a/src/Middleware/Authentication.php b/src/Middleware/Authentication.php index cbe88d7..67f3d55 100644 --- a/src/Middleware/Authentication.php +++ b/src/Middleware/Authentication.php @@ -10,7 +10,7 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Yiisoft\Auth\AuthenticatorInterface; -use Yiisoft\Auth\ChallengeInterface; +use Yiisoft\Auth\AuthenticatorWithChallengeInterface; use Yiisoft\Auth\Handler\AuthenticationFailureHandler; use Yiisoft\Strings\WildcardPattern; @@ -53,7 +53,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if ($identity === null && !$this->isOptional($request)) { $response = $this->failureHandler->handle($request); - return $this->authenticationMethod instanceof ChallengeInterface + return $this->authenticationMethod instanceof AuthenticatorWithChallengeInterface ? $this->authenticationMethod->challenge($response) : $response; }