Skip to content
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

Fix #101: Rename UserAuth to WebAuth, mark UserAuth as deprecated, add ApiAuth authentication method #105

Merged
merged 12 commits into from
Nov 11, 2024
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## 2.2.1 under development

- no changes in this release.
- Chg #101: Rename `UserAuth` to `WebAuth`, mark `UserAuth` as deprecated (@olegbaturin)
- New #101: Add `ApiAuth` authentication method (@olegbaturin)

## 2.2.0 May 07, 2024

Expand Down
81 changes: 79 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,89 @@ The `Yiisoft\User\CurrentUser` instance is stateful, so when you build long-runn
with tools like [Swoole](https://www.swoole.co.uk/) or [RoadRunner](https://roadrunner.dev/) you should reset
the state at every request. For this purpose, you can use the `clear()` method.

### Authentication methods

This package provides two authentication methods, `WebAuth` and `ApiAuth`, that implement the `Yiisoft\Auth\AuthenticationMethodInterface`. Both can be provided to the `Yiisoft\Auth\Authentication` middleware as authentication method.

#### WebAuth

`WebAuth` is used to authenticate users in the classic web applications.
If authentication is failed, it creates a new instance of the response and adds a `Location` header with a temporary redirect to the authentication URL, by default `/login`.

You can change authentication URL by calling `WebAuth::withAuthUrl()` method:

```php
use Yiisoft\User\Method\WebAuth;

$authenticationMethod = new WebAuth();

// Returns a new instance with the specified authentication URL.
$authenticationMethod = $authenticationMethod->withAuthUrl('/auth');
```

or in the [DI container](https://github.com/yiisoft/di):

```php
// config/web/di/auth.php
return [
WebAuth::class => [
'withAuthUrl()' => ['/auth'],
],
];
```

or through the parameter `authUrl` of the `yiisoft/user` config group, [yiisoft/config](https://github.com/yiisoft/config) package must be installed:

```php
// config/web/params.php
return [
'yiisoft/user' => [
'authUrl' => '/auth',
],
];
```

If the application is used along with the [yiisoft/config](https://github.com/yiisoft/config), the package is [configured](./config/di-web.php)
automatically to use `WebAuth` as default implementation of `Yiisoft\Auth\AuthenticationMethodInterface`.

#### ApiAuth

`ApiAuth` is used to authenticate users in the API clients.
If authentication is failed, it returns the response from the `Yiisoft\Auth\Middleware\Authentication::authenticationFailureHandler` handler.

To use `ApiAuth` as an authentication method, you need or provide the `ApiAuth` instance to the `Yiisoft\Auth\Middleware\Authentication` middleware:

```php
use Yiisoft\Auth\Middleware\Authentication;
use Yiisoft\User\Method\ApiAuth;

$authenticationMethod = new ApiAuth();

$middleware = new Authentication(
$authenticationMethod,
$responseFactory // PSR-17 ResponseFactoryInterface
);
```

of to define it as an implementation of `Yiisoft\Auth\AuthenticationMethodInterface` in the [DI container](https://github.com/yiisoft/di) configuration:

```php
// config/web/di/auth.php
use Yiisoft\Auth\AuthenticationMethodInterface;
use Yiisoft\User\Method\ApiAuth;

return [
AuthenticationMethodInterface::class => ApiAuth::class,
];
```

For more information about the authentication middleware and authentication methods, see the [yiisoft/auth](https://github.com/yiisoft/auth).

### Auto login through identity from request attribute

For auto login, you can use the `Yiisoft\User\Login\LoginMiddleware`. This middleware automatically logs user
in if `Yiisoft\Auth\IdentityInterface` instance presents in a request attribute. It is usually put there by
`Yiisoft\Auth\Middleware\Authentication`. For more information about the authentication middleware and
authentication methods, see the [yiisoft/auth](https://github.com/yiisoft/auth).
`Yiisoft\Auth\Middleware\Authentication`.

> Please note that `Yiisoft\Auth\Middleware\Authentication` should be located before
> `Yiisoft\User\Login\LoginMiddleware` in the middleware stack.
Expand Down
7 changes: 5 additions & 2 deletions config/di-web.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Yiisoft\User\Guest\GuestIdentityFactoryInterface;
use Yiisoft\User\Login\Cookie\CookieLogin;
use Yiisoft\User\Login\Cookie\CookieLoginMiddleware;
use Yiisoft\User\Method\WebAuth;
use Yiisoft\User\UserAuth;

/** @var array $params */
Expand All @@ -20,11 +21,13 @@
],

UserAuth::class => [
'class' => UserAuth::class,
'withAuthUrl()' => [$params['yiisoft/user']['authUrl']],
],
WebAuth::class => [
olegbaturin marked this conversation as resolved.
Show resolved Hide resolved
'withAuthUrl()' => [$params['yiisoft/user']['authUrl']],
],

AuthenticationMethodInterface::class => UserAuth::class,
AuthenticationMethodInterface::class => WebAuth::class,
GuestIdentityFactoryInterface::class => GuestIdentityFactory::class,

CookieLoginMiddleware::class => [
Expand Down
35 changes: 35 additions & 0 deletions src/Method/ApiAuth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Yiisoft\User\Method;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\Auth\AuthenticationMethodInterface;
use Yiisoft\Auth\IdentityInterface;
use Yiisoft\User\CurrentUser;

/**
* Implementation of the `AuthenticationMethodInterface` for authenticating users in the API clients.
*/
final class ApiAuth implements AuthenticationMethodInterface
{
public function __construct(private readonly CurrentUser $currentUser)
{
}

public function authenticate(ServerRequestInterface $request): ?IdentityInterface
{
if ($this->currentUser->isGuest()) {
return null;
}

return $this->currentUser->getIdentity();
}

public function challenge(ResponseInterface $response): ResponseInterface
{
return $response;
}
}
60 changes: 60 additions & 0 deletions src/Method/WebAuth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Yiisoft\User\Method;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\Auth\AuthenticationMethodInterface;
use Yiisoft\Auth\IdentityInterface;
use Yiisoft\Http\Status;
use Yiisoft\User\CurrentUser;

/**
* Implementation of the `AuthenticationMethodInterface` for authenticating users in the web applications.
*/
final class WebAuth implements AuthenticationMethodInterface
{
private string $authUrl = '/login';

public function __construct(
private readonly CurrentUser $currentUser,
private readonly ResponseFactoryInterface $responseFactory
) {
}

public function authenticate(ServerRequestInterface $request): ?IdentityInterface
{
if ($this->currentUser->isGuest()) {
return null;
}

return $this->currentUser->getIdentity();
}

/**
* {@inheritDoc}
*
* Creates a new instance of the response and adds a `Location` header with a temporary redirect.
*/
public function challenge(ResponseInterface $response): ResponseInterface
{
return $this->responseFactory
->createResponse(Status::FOUND)
->withHeader('Location', $this->authUrl);
}

/**
* Returns a new instance with the specified authentication URL.
*
* @param string $url The authentication URL.
*/
public function withAuthUrl(string $url): self
{
$new = clone $this;
$new->authUrl = $url;
return $new;
}
}
2 changes: 2 additions & 0 deletions src/UserAuth.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

/**
* Implementation of the authentication interface for the user.
*
* @deprecated Use {@see \Yiisoft\User\Method\WebAuth}. This class will be removed in the next major version.
*/
final class UserAuth implements AuthenticationMethodInterface
{
Expand Down
52 changes: 52 additions & 0 deletions tests/ApiAuthTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Yiisoft\User\Tests;

use HttpSoft\Message\Response;
use HttpSoft\Message\ServerRequest;
use PHPUnit\Framework\TestCase;
use Yiisoft\Http\Status;
use Yiisoft\Test\Support\EventDispatcher\SimpleEventDispatcher;
use Yiisoft\User\Tests\Support\MockArraySessionStorage;
use Yiisoft\User\Tests\Support\MockIdentity;
use Yiisoft\User\Tests\Support\MockIdentityRepository;
use Yiisoft\User\CurrentUser;
use Yiisoft\User\Method\ApiAuth;

final class ApiAuthTest extends TestCase
{
public function testSuccessfulAuthentication(): void
{
$user = $this->createCurrentUser();
$user->login(new MockIdentity('test-id'));
$result = (new ApiAuth($user))->authenticate(new ServerRequest());

$this->assertNotNull($result);
$this->assertSame('test-id', $result->getId());
}

public function testIdentityNotAuthenticated(): void
{
$user = $this->createCurrentUser();
$result = (new ApiAuth($user))->authenticate(new ServerRequest());

$this->assertNull($result);
}

public function testChallengeIsCorrect(): void
{
$response = new Response(Status::UNAUTHORIZED);
$user = $this->createCurrentUser();
$challenge = (new ApiAuth($user))->challenge($response);

$this->assertSame(Status::UNAUTHORIZED, $challenge->getStatusCode());
}

private function createCurrentUser(): CurrentUser
{
return (new CurrentUser(new MockIdentityRepository(), new SimpleEventDispatcher()))
->withSession(new MockArraySessionStorage());
}
}
14 changes: 7 additions & 7 deletions tests/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
use Yiisoft\User\Login\Cookie\CookieLoginMiddleware;
use Yiisoft\User\Login\LoginMiddleware;
use Yiisoft\User\Tests\Support\MockIdentityRepository;
use Yiisoft\User\UserAuth;
use Yiisoft\User\Method\WebAuth;

use function dirname;

Expand All @@ -37,10 +37,10 @@ public function testBase(): void
$this->assertInstanceOf(GuestIdentityFactory::class, $container->get(GuestIdentityFactoryInterface::class));
$this->assertInstanceOf(LoginMiddleware::class, $container->get(LoginMiddleware::class));

$userAuth = $container->get(AuthenticationMethodInterface::class);
$webAuth = $container->get(AuthenticationMethodInterface::class);

$this->assertInstanceOf(UserAuth::class, $userAuth);
$this->assertSame('/login', $this->getInaccessibleProperty($userAuth, 'authUrl'));
$this->assertInstanceOf(WebAuth::class, $webAuth);
$this->assertSame('/login', $this->getInaccessibleProperty($webAuth, 'authUrl'));

$cookieLogin = $container->get(CookieLogin::class);

Expand All @@ -67,10 +67,10 @@ public function testOverrideParams(): void
],
]);

$userAuth = $container->get(AuthenticationMethodInterface::class);
$webAuth = $container->get(AuthenticationMethodInterface::class);

$this->assertInstanceOf(UserAuth::class, $userAuth);
$this->assertSame('/override', $this->getInaccessibleProperty($userAuth, 'authUrl'));
$this->assertInstanceOf(WebAuth::class, $webAuth);
$this->assertSame('/override', $this->getInaccessibleProperty($webAuth, 'authUrl'));

$cookieLogin = $container->get(CookieLogin::class);

Expand Down
72 changes: 72 additions & 0 deletions tests/WebAuthTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace Yiisoft\User\Tests;

use HttpSoft\Message\Response;
use HttpSoft\Message\ResponseFactory;
use HttpSoft\Message\ServerRequest;
use PHPUnit\Framework\TestCase;
use Yiisoft\Http\Status;
use Yiisoft\Test\Support\EventDispatcher\SimpleEventDispatcher;
use Yiisoft\User\Tests\Support\MockArraySessionStorage;
use Yiisoft\User\Tests\Support\MockIdentity;
use Yiisoft\User\Tests\Support\MockIdentityRepository;
use Yiisoft\User\CurrentUser;
use Yiisoft\User\Method\WebAuth;

final class WebAuthTest extends TestCase
{
public function testSuccessfulAuthentication(): void
{
$user = $this->createCurrentUser();
$user->login(new MockIdentity('test-id'));
$result = (new WebAuth($user, new ResponseFactory()))->authenticate(new ServerRequest());

$this->assertNotNull($result);
$this->assertSame('test-id', $result->getId());
}

public function testIdentityNotAuthenticated(): void
{
$user = $this->createCurrentUser();
$result = (new WebAuth($user, new ResponseFactory()))->authenticate(new ServerRequest());

$this->assertNull($result);
}

public function testChallengeIsCorrect(): void
{
$response = new Response();
$user = $this->createCurrentUser();
$challenge = (new WebAuth($user, new ResponseFactory()))->challenge($response);

$this->assertSame(Status::FOUND, $challenge->getStatusCode());
$this->assertSame('/login', $challenge->getHeaderLine('Location'));
}

public function testCustomAuthUrl(): void
{
$response = new Response();
$user = $this->createCurrentUser();
$challenge = (new WebAuth($user, new ResponseFactory()))
->withAuthUrl('/custom-auth-url')
->challenge($response);

$this->assertSame('/custom-auth-url', $challenge->getHeaderLine('Location'));
}

public function testImmutability(): void
{
$original = new WebAuth($this->createCurrentUser(), new ResponseFactory());

$this->assertNotSame($original, $original->withAuthUrl('/custom-auth-url'));
}

private function createCurrentUser(): CurrentUser
{
return (new CurrentUser(new MockIdentityRepository(), new SimpleEventDispatcher()))
->withSession(new MockArraySessionStorage());
}
}
Loading