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
18 changes: 18 additions & 0 deletions docs/2-features/04-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,24 @@ final class RequestAuthenticator implements Authenticator
}
```

### HTTP middleware and route decorator

Once you've installed `auth` in your app, you'll also have a basic `MustBeAuthenticatedMiddleware` and `MustBeAuthenticated` route decorator available. You can use these in your controller actions to mark them as requiring authentication. Since these files are installed in your project code, you're free to change them to accommodate your own authentication needs.

```php
use App\Authentication\MustBeAuthenticated;
use App\Authentication\MustBeAuthenticatedMiddleware;

final readonly class BookController
{
#[MustBeAuthenticated, {:hl-type:Get:}('/books')]
public function index(): Response { /* … */ }

#[Get('/books/list', middleware: [MustBeAuthenticatedMiddleware::class])]
public function list(): Response { /* … */ }
}
```

## Access control

In most applications, it is necessary to restrict access to certain resources depending on many factors. For instance, you may want to allow only the author of a post to edit it, or allow only administrators to delete other users.
Expand Down
2 changes: 2 additions & 0 deletions packages/auth/src/Installer/AuthenticationInstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public function install(?bool $migrate = null, ?bool $oauth = null): void
{
$migration = $this->publish(__DIR__ . '/basic-user/CreateUsersTableMigration.stub.php', src_path('Authentication/CreateUsersTable.php'));
$this->publish(__DIR__ . '/basic-user/UserModel.stub.php', src_path('Authentication/User.php'));
$this->publish(__DIR__ . '/basic-user/MustBeAuthenticatedMiddleware.stub.php', src_path('Authentication/MustBeAuthenticatedMiddleware.php'));
$this->publish(__DIR__ . '/basic-user/MustBeAuthenticated.stub.php', src_path('Authentication/MustBeAuthenticated.php'));
$this->publishImports();

if ($migration && $this->shouldMigrate($migrate)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Tempest\Auth\Installer;

use Attribute;
use Tempest\Router\Route;
use Tempest\Router\RouteDecorator;

#[Attribute]
final readonly class MustBeAuthenticated implements RouteDecorator
{
public function decorate(Route $route): Route
{
$route->middleware[] = MustBeAuthenticatedMiddleware::class;

return $route;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Tempest\Auth\Installer;

use Tempest\Auth\Authentication\Authenticator;
use Tempest\Discovery\SkipDiscovery;
use Tempest\Http\Request;
use Tempest\Http\Response;
use Tempest\Http\Responses\Forbidden;
use Tempest\Router\HttpMiddleware;
use Tempest\Router\HttpMiddlewareCallable;

#[SkipDiscovery]
final readonly class MustBeAuthenticatedMiddleware implements HttpMiddleware
{
public function __construct(
private Authenticator $authenticator,
) {}

public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
{
if (! $this->authenticator->current()) {
return new Forbidden();
}

return $next($request);
}
}
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ parameters:
excludePaths:
analyse:
- packages/upgrade/tests/*/Fixtures/*.php
- packages/auth/src/Installer/basic-user
reportUnmatchedIgnoredErrors: true

disallowedFunctionCalls:
Expand Down
7 changes: 7 additions & 0 deletions src/Tempest/Framework/Testing/IntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use Closure;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Tempest\Auth\Authentication\Authenticatable;
use Tempest\Auth\Authentication\Authenticator;
use Tempest\Auth\OAuth\Testing\OAuthTester;
use Tempest\Cache\Testing\CacheTester;
use Tempest\Clock\Clock;
Expand Down Expand Up @@ -264,4 +266,9 @@ protected function assertException(string $expectedExceptionClass, Closure $hand

Assert::fail($message ?? "Expected exception {$expectedExceptionClass} was not thrown");
}

protected function login(Authenticatable $user): void
{
$this->container->get(Authenticator::class)->authenticate($user);
}
}
12 changes: 12 additions & 0 deletions tests/Integration/Auth/Installer/AuthenticationInstallerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ protected function tearDown(): void
parent::tearDown();
}

#[Test]
public function test_route_decorator_and_middleware_get_published(): void
{
$this->console
->call('install auth --force --migrate')
->assertSuccess();

$this->installer
->assertFileExists('App/Authentication/MustBeAuthenticatedMiddleware.php')
->assertFileExists('App/Authentication/MustBeAuthenticated.php');
}

#[Test]
public function install_oauth_provider_with_migrations(): void
{
Expand Down
10 changes: 1 addition & 9 deletions tests/Integration/Auth/Installer/OAuthInstallerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,8 @@ public function install_oauth_provider(
string $expectedControllerPath,
): void {
$this->console
->call('install auth --oauth')
->confirm()
->deny()
->deny()
->call('install auth --oauth --force')
->input($provider->value)
->confirm()
->confirm()
->confirm()
->confirm()
->confirm()
->assertSee('The selected OAuth provider is installed in your project')
->assertSuccess();

Expand Down
12 changes: 12 additions & 0 deletions tests/Integration/Framework/Authentication/AuthTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Tests\Tempest\Integration\Framework\Authentication;

use PHPUnit\Framework\Attributes\Test;
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;

final class AuthTest extends FrameworkIntegrationTestCase
{
#[Test]
public function test_auth(): void {}
}
6 changes: 4 additions & 2 deletions tests/Integration/Testing/ModelFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tests\Tempest\Integration\Testing;

use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use Tempest\Database\Migrations\CreateMigrationsTable;
use Tempest\DateTime\DateTime;
Expand Down Expand Up @@ -178,7 +179,7 @@ public function test_with_datetime(): void
$withDateTime = factory(WithDateTime::class)->make();

$this->assertInstanceOf(DateTime::class, $withDateTime->tempestDate);
$this->assertInstanceOf(\DateTimeImmutable::class, $withDateTime->phpDate);
$this->assertInstanceOf(DateTimeImmutable::class, $withDateTime->phpDate);
}
}

Expand All @@ -190,5 +191,6 @@ class AuthorWithEnum
class WithDateTime
{
public DateTime $tempestDate;
public \DateTimeImmutable $phpDate;

public DateTimeImmutable $phpDate;
}
Loading