Skip to content

Commit 527f0ae

Browse files
authored
feat(auth): add middleware and route decorator to auth installer (#2187)
1 parent a729206 commit 527f0ae

10 files changed

Lines changed: 103 additions & 11 deletions

File tree

docs/2-features/04-authentication.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,24 @@ final class RequestAuthenticator implements Authenticator
223223
}
224224
```
225225

226+
### HTTP middleware and route decorator
227+
228+
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.
229+
230+
```php
231+
use App\Authentication\MustBeAuthenticated;
232+
use App\Authentication\MustBeAuthenticatedMiddleware;
233+
234+
final readonly class BookController
235+
{
236+
#[MustBeAuthenticated, {:hl-type:Get:}('/books')]
237+
public function index(): Response { /* … */ }
238+
239+
#[Get('/books/list', middleware: [MustBeAuthenticatedMiddleware::class])]
240+
public function list(): Response { /* … */ }
241+
}
242+
```
243+
226244
## Access control
227245

228246
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.

packages/auth/src/Installer/AuthenticationInstaller.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public function install(?bool $migrate = null, ?bool $oauth = null): void
2525
{
2626
$migration = $this->publish(__DIR__ . '/basic-user/CreateUsersTableMigration.stub.php', src_path('Authentication/CreateUsersTable.php'));
2727
$this->publish(__DIR__ . '/basic-user/UserModel.stub.php', src_path('Authentication/User.php'));
28+
$this->publish(__DIR__ . '/basic-user/MustBeAuthenticatedMiddleware.stub.php', src_path('Authentication/MustBeAuthenticatedMiddleware.php'));
29+
$this->publish(__DIR__ . '/basic-user/MustBeAuthenticated.stub.php', src_path('Authentication/MustBeAuthenticated.php'));
2830
$this->publishImports();
2931

3032
if ($migration && $this->shouldMigrate($migrate)) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Tempest\Auth\Installer;
4+
5+
use Attribute;
6+
use Tempest\Router\Route;
7+
use Tempest\Router\RouteDecorator;
8+
9+
#[Attribute]
10+
final readonly class MustBeAuthenticated implements RouteDecorator
11+
{
12+
public function decorate(Route $route): Route
13+
{
14+
$route->middleware[] = MustBeAuthenticatedMiddleware::class;
15+
16+
return $route;
17+
}
18+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Tempest\Auth\Installer;
4+
5+
use Tempest\Auth\Authentication\Authenticator;
6+
use Tempest\Discovery\SkipDiscovery;
7+
use Tempest\Http\Request;
8+
use Tempest\Http\Response;
9+
use Tempest\Http\Responses\Forbidden;
10+
use Tempest\Router\HttpMiddleware;
11+
use Tempest\Router\HttpMiddlewareCallable;
12+
13+
#[SkipDiscovery]
14+
final readonly class MustBeAuthenticatedMiddleware implements HttpMiddleware
15+
{
16+
public function __construct(
17+
private Authenticator $authenticator,
18+
) {}
19+
20+
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
21+
{
22+
if (! $this->authenticator->current()) {
23+
return new Forbidden();
24+
}
25+
26+
return $next($request);
27+
}
28+
}

phpstan.neon.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ parameters:
2727
excludePaths:
2828
analyse:
2929
- packages/upgrade/tests/*/Fixtures/*.php
30+
- packages/auth/src/Installer/basic-user
3031
reportUnmatchedIgnoredErrors: true
3132

3233
disallowedFunctionCalls:

src/Tempest/Framework/Testing/IntegrationTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use Closure;
88
use PHPUnit\Framework\Assert;
99
use PHPUnit\Framework\TestCase;
10+
use Tempest\Auth\Authentication\Authenticatable;
11+
use Tempest\Auth\Authentication\Authenticator;
1012
use Tempest\Auth\OAuth\Testing\OAuthTester;
1113
use Tempest\Cache\Testing\CacheTester;
1214
use Tempest\Clock\Clock;
@@ -264,4 +266,9 @@ protected function assertException(string $expectedExceptionClass, Closure $hand
264266

265267
Assert::fail($message ?? "Expected exception {$expectedExceptionClass} was not thrown");
266268
}
269+
270+
protected function login(Authenticatable $user): void
271+
{
272+
$this->container->get(Authenticator::class)->authenticate($user);
273+
}
267274
}

tests/Integration/Auth/Installer/AuthenticationInstallerTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ protected function tearDown(): void
2929
parent::tearDown();
3030
}
3131

32+
#[Test]
33+
public function test_route_decorator_and_middleware_get_published(): void
34+
{
35+
$this->console
36+
->call('install auth --force --migrate')
37+
->assertSuccess();
38+
39+
$this->installer
40+
->assertFileExists('App/Authentication/MustBeAuthenticatedMiddleware.php')
41+
->assertFileExists('App/Authentication/MustBeAuthenticated.php');
42+
}
43+
3244
#[Test]
3345
public function install_oauth_provider_with_migrations(): void
3446
{

tests/Integration/Auth/Installer/OAuthInstallerTest.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,8 @@ public function install_oauth_provider(
4141
string $expectedControllerPath,
4242
): void {
4343
$this->console
44-
->call('install auth --oauth')
45-
->confirm()
46-
->deny()
47-
->deny()
44+
->call('install auth --oauth --force')
4845
->input($provider->value)
49-
->confirm()
50-
->confirm()
51-
->confirm()
52-
->confirm()
53-
->confirm()
5446
->assertSee('The selected OAuth provider is installed in your project')
5547
->assertSuccess();
5648

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\Framework\Authentication;
4+
5+
use PHPUnit\Framework\Attributes\Test;
6+
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
7+
8+
final class AuthTest extends FrameworkIntegrationTestCase
9+
{
10+
#[Test]
11+
public function test_auth(): void {}
12+
}

tests/Integration/Testing/ModelFactoryTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Tests\Tempest\Integration\Testing;
44

5+
use DateTimeImmutable;
56
use PHPUnit\Framework\Attributes\Test;
67
use Tempest\Database\Migrations\CreateMigrationsTable;
78
use Tempest\DateTime\DateTime;
@@ -178,7 +179,7 @@ public function test_with_datetime(): void
178179
$withDateTime = factory(WithDateTime::class)->make();
179180

180181
$this->assertInstanceOf(DateTime::class, $withDateTime->tempestDate);
181-
$this->assertInstanceOf(\DateTimeImmutable::class, $withDateTime->phpDate);
182+
$this->assertInstanceOf(DateTimeImmutable::class, $withDateTime->phpDate);
182183
}
183184
}
184185

@@ -190,5 +191,6 @@ class AuthorWithEnum
190191
class WithDateTime
191192
{
192193
public DateTime $tempestDate;
193-
public \DateTimeImmutable $phpDate;
194+
195+
public DateTimeImmutable $phpDate;
194196
}

0 commit comments

Comments
 (0)