From 53c85096be7816a841ab3c0b1b782e3dc8b01abb Mon Sep 17 00:00:00 2001 From: klsoft-web Date: Fri, 24 Jul 2026 14:52:21 +0300 Subject: [PATCH] Segregate the methods of the AuthenticationMethodInterface --- CHANGELOG.md | 1 + README.md | 22 +++++------ src/AuthenticationMethodInterface.php | 25 +----------- src/AuthenticatorInterface.php | 22 +++++++++++ src/ChallengeInterface.php | 23 +++++++++++ src/Method/Composite.php | 15 +++++--- src/Method/HttpBasic.php | 5 ++- src/Method/HttpBearer.php | 3 +- src/Method/HttpCookie.php | 10 +---- src/Method/HttpHeader.php | 10 +---- src/Method/QueryParameter.php | 10 +---- src/Middleware/Authentication.php | 34 +++++++---------- tests/AuthenticationMiddlewareTest.php | 53 +++++++++----------------- tests/Method/HttpCookieTest.php | 10 ----- tests/Method/HttpHeaderTest.php | 15 -------- tests/Method/QueryParameterTest.php | 12 ------ 16 files changed, 110 insertions(+), 160 deletions(-) create mode 100644 src/AuthenticatorInterface.php create mode 100644 src/ChallengeInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b5df46..ea5537f 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: Segregate the methods of the AuthenticationMethodInterface (@klsoft-web) ## 3.2.1 December 17, 2025 diff --git a/README.md b/README.md index 91c4fb6..5f9b598 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,12 @@ Configure a middleware and add it to your middleware stack: ```php $identityRepository = getIdentityWithTokenRepository(); // \Yiisoft\Auth\IdentityRepositoryInterface -$authenticationMethod = new \Yiisoft\Auth\Method\HttpBasic($identityRepository); +$authenticator = new \Yiisoft\Auth\Method\HttpBasic($identityRepository); +$failureHandler = new \Yiisoft\Auth\Handler\AuthenticationFailureHandler($responseFactory); $middleware = new \Yiisoft\Auth\Middleware\Authentication( - $authenticationMethod, - $responseFactory, // PSR-17 ResponseFactoryInterface - $failureHandler // optional, \Yiisoft\Auth\Handler\AuthenticationFailureHandler by default + $authenticator, + $failureHandler ); $middlewareDispatcher->addMiddleware($middleware); @@ -47,7 +47,7 @@ $middlewareDispatcher->addMiddleware($middleware); In order to get an identity instance in the following middleware use `getAttribute()` method of the request instance: ```php -public function actionIndex(\Psr\Http\Message\ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface +public function index(\Psr\Http\Message\ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface { $identity = $request->getAttribute(\Yiisoft\Auth\Middleware\Authentication::class); // ... @@ -60,7 +60,7 @@ Basic HTTP authentication is typically used for entering login and password in t Credentials are passed as `$_SERVER['PHP_AUTH_USER']` and `$_SERVER['PHP_AUTH_PW']`. ```php -$authenticationMethod = (new \Yiisoft\Auth\Method\HttpBasic($identityRepository)) +$authenticator = (new \Yiisoft\Auth\Method\HttpBasic($identityRepository)) ->withRealm('Admin') ->withAuthenticationCallback(static function ( ?string $username, @@ -79,7 +79,7 @@ Custom authentication callback set in the above is the same as default behavior Bearer HTTP authentication is typically used in APIs. Authentication token is passed in `WWW-Authenticate` header. ```php -$authenticationMethod = new \Yiisoft\Auth\Method\HttpBearer($identityRepository); +$authenticator = new \Yiisoft\Auth\Method\HttpBearer($identityRepository); ``` ### Custom HTTP header authentication @@ -87,7 +87,7 @@ $authenticationMethod = new \Yiisoft\Auth\Method\HttpBearer($identityRepository) Custom HTTP header could be used if you do not want to leverage bearer token authentication: ```php - $authenticationMethod = (new \Yiisoft\Auth\Method\HttpHeader($identityRepository)) + $authenticator = (new \Yiisoft\Auth\Method\HttpHeader($identityRepository)) ->withHeaderName('X-Api-Key') ->withPattern('/(.*)/'); // default ``` @@ -100,14 +100,14 @@ This authentication method is mainly used by clients unable to send headers. In we advise not to use it. ```php -$authenticationMethod = (new \Yiisoft\Auth\Method\QueryParameter($identityRepository)) +$authenticator = (new \Yiisoft\Auth\Method\QueryParameter($identityRepository)) ->withParameterName('token'); ``` ### HTTP cookie authentication ```php -$authenticationMethod = (new \Yiisoft\Auth\Method\HttpCookie($identityRepository)) +$authenticator = (new \Yiisoft\Auth\Method\HttpCookie($identityRepository)) ->withCookieName('access-token'); ``` @@ -118,7 +118,7 @@ Typical authentication for websites by storing a token in a browser cookie. To use multiple authentication methods, use `Yiisoft\Auth\Method\Composite`: ```php -$authenticationMethod = new \Yiisoft\Auth\Method\Composite([ +$authenticator = new \Yiisoft\Auth\Method\Composite([ $bearerAuthenticationMethod, $basicAuthenticationMethod ]); diff --git a/src/AuthenticationMethodInterface.php b/src/AuthenticationMethodInterface.php index dc40212..5fea204 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 Use AuthenticatorInterface and optionally 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..3be8327 --- /dev/null +++ b/src/AuthenticatorInterface.php @@ -0,0 +1,22 @@ +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 +42,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/Method/HttpBasic.php b/src/Method/HttpBasic.php index 52c000d..1e0d4af 100644 --- a/src/Method/HttpBasic.php +++ b/src/Method/HttpBasic.php @@ -6,7 +6,8 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Yiisoft\Auth\AuthenticationMethodInterface; +use Yiisoft\Auth\AuthenticatorInterface; +use Yiisoft\Auth\ChallengeInterface; 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 AuthenticatorInterface, ChallengeInterface { private string $realm = 'api'; private ?string $tokenType = null; diff --git a/src/Method/HttpBearer.php b/src/Method/HttpBearer.php index e3f1487..327f5d4 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\ChallengeInterface; 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 ChallengeInterface { protected string $headerName = Header::AUTHORIZATION; diff --git a/src/Method/HttpCookie.php b/src/Method/HttpCookie.php index 14d1a9b..e1f22cf 100644 --- a/src/Method/HttpCookie.php +++ b/src/Method/HttpCookie.php @@ -4,9 +4,8 @@ namespace Yiisoft\Auth\Method; -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 +14,7 @@ * * @see https://tools.ietf.org/html/rfc6265 */ -final class HttpCookie implements AuthenticationMethodInterface +final class HttpCookie implements AuthenticatorInterface { private string $cookieName = 'access-token'; private ?string $tokenType = null; @@ -35,11 +34,6 @@ public function authenticate(ServerRequestInterface $request): ?IdentityInterfac return $this->identityRepository->findIdentityByToken($authToken, $this->tokenType); } - public function challenge(ResponseInterface $response): ResponseInterface - { - return $response; - } - /** * @psalm-immutable */ diff --git a/src/Method/HttpHeader.php b/src/Method/HttpHeader.php index 91f7ea3..d9ac425 100644 --- a/src/Method/HttpHeader.php +++ b/src/Method/HttpHeader.php @@ -5,9 +5,8 @@ namespace Yiisoft\Auth\Method; use JetBrains\PhpStorm\Language; -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 +19,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 AuthenticatorInterface { protected string $headerName = 'X-Api-Key'; @@ -43,11 +42,6 @@ public function authenticate(ServerRequestInterface $request): ?IdentityInterfac return null; } - public function challenge(ResponseInterface $response): ResponseInterface - { - return $response; - } - /** * @param string $name The HTTP header name. * diff --git a/src/Method/QueryParameter.php b/src/Method/QueryParameter.php index ff543ad..59d7c48 100644 --- a/src/Method/QueryParameter.php +++ b/src/Method/QueryParameter.php @@ -4,9 +4,8 @@ namespace Yiisoft\Auth\Method; -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 +14,7 @@ /** * QueryParameter supports the authentication based on the access token passed through a query parameter. */ -final class QueryParameter implements AuthenticationMethodInterface +final class QueryParameter implements AuthenticatorInterface { private string $parameterName = 'access-token'; private ?string $tokenType = null; @@ -32,11 +31,6 @@ public function authenticate(ServerRequestInterface $request): ?IdentityInterfac return null; } - public function challenge(ResponseInterface $response): ResponseInterface - { - return $response; - } - /** * @param string $name The parameter name for passing the access token. * diff --git a/src/Middleware/Authentication.php b/src/Middleware/Authentication.php index 9f70db1..b1cb99e 100644 --- a/src/Middleware/Authentication.php +++ b/src/Middleware/Authentication.php @@ -4,27 +4,22 @@ namespace Yiisoft\Auth\Middleware; -use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; 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; /** * Authentication middleware tries to authenticate and identity using request data. * If identity is found, it is set to request attribute allowing further middleware to obtain and use it. - * If identity is not found failure handler is called. By default it is {@see AuthenticationFailureHandler}. + * If identity is not found failure handler is called. By default, it is {@see AuthenticationFailureHandler}. */ -final class Authentication implements MiddlewareInterface +class Authentication implements MiddlewareInterface { - /** - * @var RequestHandlerInterface A handler that is called when there is a failure authenticating an identity. - */ - private RequestHandlerInterface $failureHandler; - /** * @var array Patterns to match to consider the given request URI path optional. */ @@ -35,24 +30,21 @@ final class Authentication implements MiddlewareInterface private array $wildcards = []; public function __construct( - private AuthenticationMethodInterface $authenticationMethod, - ResponseFactoryInterface $responseFactory, - ?RequestHandlerInterface $authenticationFailureHandler = null, - ) { - $this->failureHandler = $authenticationFailureHandler ?? new AuthenticationFailureHandler( - $responseFactory, - ); - } + private AuthenticatorInterface $authenticator, + private RequestHandlerInterface $authenticationFailureHandler, + ) {} public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $identity = $this->authenticationMethod->authenticate($request); + $identity = $this->authenticator->authenticate($request); $request = $request->withAttribute(self::class, $identity); if ($identity === null && !$this->isOptional($request)) { - return $this->authenticationMethod->challenge( - $this->failureHandler->handle($request), - ); + $response = $this->authenticationFailureHandler->handle($request); + + return $this->authenticator instanceof ChallengeInterface + ? $this->authenticator->challenge($response) + : $response; } return $handler->handle($request); diff --git a/tests/AuthenticationMiddlewareTest.php b/tests/AuthenticationMiddlewareTest.php index 51e21b1..0ae1955 100644 --- a/tests/AuthenticationMiddlewareTest.php +++ b/tests/AuthenticationMiddlewareTest.php @@ -13,7 +13,8 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use Yiisoft\Auth\AuthenticationMethodInterface; +use Yiisoft\Auth\AuthenticatorInterface; +use Yiisoft\Auth\Handler\AuthenticationFailureHandler; use Yiisoft\Auth\IdentityInterface; use Yiisoft\Auth\Middleware\Authentication; use Yiisoft\Http\Status; @@ -22,13 +23,16 @@ final class AuthenticationMiddlewareTest extends TestCase { private ResponseFactoryInterface $responseFactory; - /** @var AuthenticationMethodInterface|MockObject */ - private AuthenticationMethodInterface $authenticationMethod; + /** @var AuthenticatorInterface&MockObject */ + private AuthenticatorInterface $authenticator; + + private AuthenticationFailureHandler $authenticationFailureHandler; protected function setUp(): void { $this->responseFactory = new Psr17Factory(); - $this->authenticationMethod = $this->createMock(AuthenticationMethodInterface::class); + $this->authenticator = $this->createMock(AuthenticatorInterface::class); + $this->authenticationFailureHandler = new AuthenticationFailureHandler($this->responseFactory); } public function testShouldAuthenticateAndSetAttribute(): void @@ -36,7 +40,7 @@ public function testShouldAuthenticateAndSetAttribute(): void $request = new ServerRequest('GET', '/'); $identity = $this->createMock(IdentityInterface::class); - $this->authenticationMethod + $this->authenticator ->expects($this->once()) ->method('authenticate') ->willReturn($identity); @@ -53,7 +57,7 @@ function (ServerRequestInterface $request) use ($identity) { }, ); - $auth = new Authentication($this->authenticationMethod, $this->responseFactory); + $auth = new Authentication($this->authenticator, $this->authenticationFailureHandler); $auth->process($request, $handler); } @@ -70,7 +74,7 @@ public function testShouldSkipCheckForOptionalPath(string $path): void { $request = new ServerRequest('GET', $path); - $this->authenticationMethod + $this->authenticator ->expects($this->once()) ->method('authenticate') ->willReturn(null); @@ -80,7 +84,7 @@ public function testShouldSkipCheckForOptionalPath(string $path): void ->expects($this->once()) ->method('handle'); - $auth = (new Authentication($this->authenticationMethod, $this->responseFactory)) + $auth = (new Authentication($this->authenticator, $this->authenticationFailureHandler)) ->withOptionalPatterns([$path]); $auth->process($request, $handler); } @@ -88,50 +92,31 @@ public function testShouldSkipCheckForOptionalPath(string $path): void public function testShouldNotExecuteHandlerAndReturn401OnAuthenticationFailure(): void { $request = new ServerRequest('GET', '/'); - $header = 'Authenticated'; - $headerValue = 'false'; - $this->authenticationMethod + $this->authenticator ->expects($this->once()) ->method('authenticate') ->willReturn(null); - $this->authenticationMethod - ->expects($this->once()) - ->method('challenge') - ->willReturnCallback( - static fn(ResponseInterface $response) => $response->withHeader($header, $headerValue), - ); - $handler = $this->createMock(RequestHandlerInterface::class); $handler ->expects($this->never()) ->method('handle'); - $auth = new Authentication($this->authenticationMethod, $this->responseFactory); + $auth = new Authentication($this->authenticator, $this->authenticationFailureHandler); $response = $auth->process($request, $handler); $this->assertEquals(401, $response->getStatusCode()); - $this->assertEquals($headerValue, $response->getHeaderLine($header)); } public function testCustomAuthenticationFailureResponse(): void { $request = new ServerRequest('GET', '/'); - $header = 'Authenticated'; - $headerValue = 'false'; - $this->authenticationMethod + $this->authenticator ->expects($this->once()) ->method('authenticate') ->willReturn(null); - $this->authenticationMethod - ->expects($this->once()) - ->method('challenge') - ->willReturnCallback( - static fn(ResponseInterface $response) => $response->withHeader($header, $headerValue), - ); - $handler = $this->createMock(RequestHandlerInterface::class); $handler ->expects($this->never()) @@ -140,21 +125,19 @@ public function testCustomAuthenticationFailureResponse(): void $failureResponse = 'test custom response'; $auth = new Authentication( - $this->authenticationMethod, - $this->responseFactory, + $this->authenticator, $this->createAuthenticationFailureHandler($failureResponse), ); $response = $auth->process($request, $handler); $this->assertEquals(401, $response->getStatusCode()); - $this->assertEquals($headerValue, $response->getHeaderLine($header)); $this->assertEquals($failureResponse, (string) $response->getBody()); } public function testImmutability(): void { $original = new Authentication( - $this->authenticationMethod, - $this->responseFactory, + $this->authenticator, + $this->authenticationFailureHandler, ); $this->assertNotSame($original, $original->withOptionalPatterns(['test'])); diff --git a/tests/Method/HttpCookieTest.php b/tests/Method/HttpCookieTest.php index 1b79007..2f963ac 100644 --- a/tests/Method/HttpCookieTest.php +++ b/tests/Method/HttpCookieTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\Auth\Tests\Method; -use Nyholm\Psr7\Response; use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; @@ -60,15 +59,6 @@ public function testIdentityNotFoundByToken(): void ); } - public function testChallengeImmutabilityStatus(): void - { - $response = new Response(400); - $identityRepository = new FakeIdentityRepository($this->createIdentity()); - $authenticationMethod = new HttpCookie($identityRepository); - - $this->assertSame($response, $authenticationMethod->challenge($response)); - } - public function testCustomTokenParam(): void { $identityRepository = new FakeIdentityRepository($this->createIdentity()); diff --git a/tests/Method/HttpHeaderTest.php b/tests/Method/HttpHeaderTest.php index 0519c1c..d225174 100644 --- a/tests/Method/HttpHeaderTest.php +++ b/tests/Method/HttpHeaderTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\Auth\Tests\Method; -use Nyholm\Psr7\Response; use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; @@ -39,20 +38,6 @@ public function testIdentityNotFoundByToken(): void ); } - public function testChallengeIsCorrect(): void - { - $response = new Response(400); - $identityRepository = new FakeIdentityRepository($this->createIdentity()); - $authenticationMethod = new HttpHeader($identityRepository); - - $this->assertEquals( - 400, - $authenticationMethod - ->challenge($response) - ->getStatusCode(), - ); - } - public function testEmptyTokenHeader(): void { $identityRepository = new FakeIdentityRepository($this->createIdentity()); diff --git a/tests/Method/QueryParameterTest.php b/tests/Method/QueryParameterTest.php index a97af9c..6e0412c 100644 --- a/tests/Method/QueryParameterTest.php +++ b/tests/Method/QueryParameterTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\Auth\Tests\Method; -use Nyholm\Psr7\Response; use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; @@ -62,17 +61,6 @@ public function testInvalidTypeToken(): void $this->assertEmpty($identityRepository->getCallParams()); } - public function testChallengeIsCorrect(): void - { - $response = new Response(400); - $identityRepository = new FakeIdentityRepository($this->createIdentity()); - $authenticationMethod = new QueryParameter($identityRepository); - - $this->assertEquals(400, $authenticationMethod - ->challenge($response) - ->getStatusCode()); - } - public function testCustomTokenParam(): void { $identityRepository = new FakeIdentityRepository($this->createIdentity());