diff --git a/.roave-backward-compatibility-check.xml b/.roave-backward-compatibility-check.xml index b399121..f2a3524 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$# diff --git a/CHANGELOG.md b/CHANGELOG.md index 889d553..9bb027c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 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) diff --git a/README.md b/README.md index 91c4fb6..46c880a 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\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 dc40212..adeb27a 100644 --- a/src/AuthenticationMethodInterface.php +++ b/src/AuthenticationMethodInterface.php @@ -4,30 +4,8 @@ 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} or, if a challenge is needed, + * {@see AuthenticatorWithChallengeInterface}. */ -interface AuthenticationMethodInterface -{ - /** - * 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; -} +interface AuthenticationMethodInterface extends AuthenticatorWithChallengeInterface {} diff --git a/src/AuthenticatorInterface.php b/src/AuthenticatorInterface.php new file mode 100644 index 0000000..24b54b6 --- /dev/null +++ b/src/AuthenticatorInterface.php @@ -0,0 +1,21 @@ +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 AuthenticatorWithChallengeInterface) { + $response = $method->challenge($response); + } } return $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 9f70db1..67f3d55 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\AuthenticatorWithChallengeInterface; 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 AuthenticatorWithChallengeInterface + ? $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(