Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .roave-backward-compatibility-check.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@
xsi:noNamespaceSchemaLocation="vendor/roave/backward-compatibility-check/Resources/schema.xsd">
<baseline>
<ignored-regex>#\[BC\] SKIPPED: Roave\\BetterReflection\\Reflection\\ReflectionClass "Yiisoft\\Yii\\Debug#</ignored-regex>
<!--
AuthenticationMethodInterface now extends AuthenticatorInterface, so accepting AuthenticatorInterface widens
the constructor parameter type. Roave compares the two package versions independently and cannot resolve this
newly introduced cross-version parent relationship as contravariant.
-->
<ignored-regex>#^\[BC\] CHANGED: The parameter \$authenticationMethod of Yiisoft\\Auth\\Middleware\\Authentication\#__construct\(\) changed from Yiisoft\\Auth\\AuthenticationMethodInterface to a non-contravariant Yiisoft\\Auth\\AuthenticatorInterface$#</ignored-regex>
</baseline>
</roave-bc-check>
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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

Expand Down
28 changes: 3 additions & 25 deletions src/AuthenticationMethodInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
21 changes: 21 additions & 0 deletions src/AuthenticatorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Auth;

use Psr\Http\Message\ServerRequestInterface;

/**
* 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
{
/**
* @return IdentityInterface|null An identity or null if there is no match.
*/
public function authenticate(ServerRequestInterface $request): ?IdentityInterface;
}
23 changes: 23 additions & 0 deletions src/AuthenticatorWithChallengeInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Auth;

use Psr\Http\Message\ResponseInterface;

/**
* Authenticates an identity using information available in a request and adds an authentication challenge
* to a response upon failure.
*/
interface AuthenticatorWithChallengeInterface extends AuthenticatorInterface
{
/**
* Adds an authentication challenge to the response.
*
* @param ResponseInterface $response Response to modify.
*
* @return ResponseInterface Modified response.
*/
public function challenge(ResponseInterface $response): ResponseInterface;
}
14 changes: 9 additions & 5 deletions src/Method/Composite.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\Auth\AuthenticationMethodInterface;
use Yiisoft\Auth\AuthenticatorInterface;
use Yiisoft\Auth\AuthenticatorWithChallengeInterface;
use Yiisoft\Auth\IdentityInterface;
use RuntimeException;

/**
* Composite allows multiple authentication methods at the same time.
*/
final class Composite implements AuthenticationMethodInterface
final class Composite implements AuthenticationMethodInterface, AuthenticatorWithChallengeInterface
{
/**
* @param AuthenticationMethodInterface[] $methods
* @param AuthenticatorInterface[] $methods
*/
public function __construct(
private readonly array $methods,
Expand All @@ -25,8 +27,8 @@ public function __construct(
public function authenticate(ServerRequestInterface $request): ?IdentityInterface
{
foreach ($this->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);
Expand All @@ -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;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Method/HttpBasic.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/Method/HttpBearer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
namespace Yiisoft\Auth\Method;

use Psr\Http\Message\ResponseInterface;
use Yiisoft\Auth\AuthenticatorWithChallengeInterface;
use Yiisoft\Http\Header;

/**
* Authentication method based on HTTP Bearer token.
*
* @see https://tools.ietf.org/html/rfc6750
*/
final class HttpBearer extends HttpHeader
final class HttpBearer extends HttpHeader implements AuthenticatorWithChallengeInterface
{
protected string $headerName = Header::AUTHORIZATION;

Expand Down
7 changes: 6 additions & 1 deletion src/Method/HttpCookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion src/Method/HttpHeader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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';

Expand All @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion src/Method/QueryParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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;
Expand Down
13 changes: 8 additions & 5 deletions src/Middleware/Authentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
) {
Expand All @@ -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);
Expand Down
20 changes: 20 additions & 0 deletions tests/AuthenticationMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
Loading