Skip to content

Commit 19cb4ac

Browse files
committed
docs(auth): overhaul auth documentation
1 parent 62337ce commit 19cb4ac

File tree

1 file changed

+263
-53
lines changed

1 file changed

+263
-53
lines changed
Lines changed: 263 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,315 @@
11
---
2-
title: Authentication and authorization
3-
keywords: "Experimental"
2+
title: Authentication
3+
description: "Learn how to authenticate models, implement access control, and secure your application with Tempest's flexible authentication system."
44
---
55

6-
:::warning
7-
The authentication and authorization implementations of Tempest are currently experimental. Although you can use them, please note that they are not covered by our backwards compatibility promise.
8-
:::
9-
106
## Overview
117

12-
Logging in (authentication) and verifying whether a user is allowed to perform a specific action (authorization) are two crucial parts of any web application. Tempest comes with a built-in authenticator and authorizer, as well as a base `User` and `Permission` model (if you want to).
8+
Tempest provides an authentication implementation designed to be flexible, not assuming an authenticatable model is a user. This means you can use it for API keys, service accounts, or any other system that requires authentication.
9+
10+
Additionally, Tempest provides a [policy-based access control](#access-control) implementation that allows you to define fine-grained permissions for your resources.
11+
12+
## Quick start
13+
14+
Tempest does not assume that all applications have users, but it is the most common case. For this reason, we provide the ability to publish a basic user model and its migration.
15+
16+
```sh sh
17+
./tempest install auth
18+
```
19+
20+
After publishing, you may run `./tempest migrate`. You now have the building blocks for your authentication.
1321

1422
## Authentication
1523

16-
Logging in a user can be done with the `Authenticator` class:
24+
Tempest's authentication is flexible enough not to assume that an authenticatable model is a user. If your application uses a different system for authentication, such as an API key or a service account, you have the ability to create such a model while preserving the correct nomenclature.
1725

18-
```php
19-
// app/AuthController.php
26+
To register an authenticatable model, you may create a class that implements the {b`Tempest\Auth\Authentication\CanAuthenticate`} interface. This interface is automatically discovered by Tempest.
2027

21-
use Tempest\Auth\Authenticator;
22-
use Tempest\Http\Request;
23-
use Tempest\Http\Response;
24-
use Tempest\Http\Responses\Redirect;
28+
```php app/Authentication/User.php
29+
use Tempest\Auth\Authentication\CanAuthenticate;
30+
use Tempest\Database\PrimaryKey;
31+
use Tempest\Database\Hashed;
2532

26-
final readonly class AuthController
33+
final class User implements CanAuthenticate
2734
{
35+
public PrimaryKey $id;
36+
2837
public function __construct(
29-
private Authenticator $authenticator
38+
public string $email,
39+
#[Hashed]
40+
public ?string $password,
41+
) {}
42+
}
43+
```
44+
45+
Note that if you use the default [database authenticatable resolver](#custom-authenticatable-resolver), the model must have at least a {b`Tempest\Database\PrimaryKey`} property—it will be used to uniquely identify the model in the database.
46+
47+
### Authenticating a model
48+
49+
Authenticating a model—in most cases, a user—is usually done in a controller. Tempest provides an {b`Tempest\Auth\Authentication\Authenticator`} that may authenticate, deauthenticate, and access the currently authenticated model.
50+
51+
Because there are a lot of different ways to authenticate users or systems, Tempest doesn't provide the logic to verify authentication credentials. In the case of a user, you may use the {b`Tempest\Cryptography\Password\PasswordHasher`} for this purpose.
52+
53+
```php app/Authentication/AuthenticationController.php
54+
use Tempest\Auth\Authentication\Authenticator;
55+
use Tempest\Cryptography\Password\PasswordHasher;
56+
57+
final readonly class AuthenticationController
58+
{
59+
public function __construct(
60+
private Authenticator $authenticator,
61+
private PasswordHasher $passwordHasher,
3062
) {}
3163

3264
#[Post('/login')]
33-
public function login(Request $request): Response
65+
public function login(LoginRequest $request): Redirect
3466
{
35-
$user = // …
67+
$user = query(User::class)
68+
->select()
69+
->where('email', $request->email)
70+
->first();
71+
72+
if (! $user || ! $this->passwordHasher->verify($request->password, $user->password)) {
73+
return new Redirect('/login')->flash('error', 'Invalid credentials');
74+
}
3675

37-
$this->authenticator->login($user);
76+
$this->authenticator->authenticate($user);
3877

3978
return new Redirect('/');
4079
}
80+
81+
#[Post('/logout')]
82+
public function logout(): Redirect
83+
{
84+
$this->authenticator->deauthenticate();
85+
86+
return new Redirect('/login');
87+
}
4188
}
4289
```
4390

44-
Note that Tempest currently doesn't provide user management support (resolving a user from a request, user registration, password reset flow, etc.).
91+
### Accessing the authenticated model
4592

46-
## Authorization
93+
You may access the currently authenticated model by injecting the {b`Tempest\Auth\Authentication\Authenticator`}. The authenticator provides a `current()` method that returns the currently authenticated model, or `null` if no model is authenticated.
4794

48-
You can protect controller routes using the `#[Allow]` attribute:
95+
```php app/ProfileController.php
96+
use Tempest\Auth\Authentication\Authenticator;
97+
use Tempest\Router\Get;
98+
use Tempest\View\View;
99+
use function Tempest\view;
49100

50-
```php
51-
// app/AdminController.php
101+
final readonly class ProfileController
102+
{
103+
public function __construct(
104+
private Authenticator $authenticator,
105+
) {}
52106

53-
use Tempest\Auth\Allow;
54-
use Tempest\Http\Response;
107+
#[Get('/profile', middleware: [MustBeAuthenticated::class])]
108+
public function show(): View
109+
{
110+
return view('profile.view.php', user: $this->authenticator->current());
111+
}
112+
}
113+
```
114+
115+
Alternatively, you may also inject the model directly. For instance, if you have a `User` model implementing `CanAuthenticate`, it can be injected as a dependency:
55116

56-
final readonly class AdminController
117+
```php app/ProfileController.php
118+
final readonly class ProfileController
57119
{
58-
#[Allow('permission')]
59-
public function index(): Response
120+
public function __construct(
121+
private User $user,
122+
) {}
123+
124+
#[Get('/profile', middleware: [MustBeAuthenticated::class])]
125+
public function show(): View
60126
{
61-
// …
127+
return view('profile.view.php', user: $this->user);
62128
}
63129
}
64130
```
65131

66-
Tempest uses a permission-based authorizer. That means that, in order for users to be allowed access to a route, they'll need to be granted the right permission. Permissions can be represented as strings or enums:
132+
:::warning
133+
In situations where the model might not be authenticated—for instance, in a route that is not protected by a middleware, you will need to make the property nullable.
134+
:::
67135

68-
```php
69-
// app/AdminController.php
136+
### Custom authenticatable resolver
137+
138+
The authenticatable resolver is used internally by the authenticator to resolve an unique identifier from a model and the other way around. Typically, applications use a database to store users, but you can implement custom resolvers to fetch users from other sources, such as LDAP or external APIs.
139+
140+
Tempest provides a {b`Tempest\Auth\Authentication\DatabaseAuthenticatableResolver`}, which is used by default. However, you may implement your own resolver by implementing the {b`Tempest\Auth\Authentication\AuthenticatableResolver`} interface.
70141

71-
use Tempest\Auth\Allow;
72-
use Tempest\Http\Response;
142+
```php app/Authentication/LdapAuthenticatableResolver.php
143+
use Tempest\Auth\Authentication\AuthenticatableResolver;
144+
use Tempest\Auth\Authentication\CanAuthenticate;
145+
use App\Authentication\User;
73146

74-
final readonly class AdminController
147+
final readonly class LdapAuthenticatableResolver implements AuthenticatableResolver
75148
{
76-
#[Allow(UserPermission::ADMIN)]
77-
public function index(): Response
149+
public function __construct(
150+
private LdapClient $ldap,
151+
) {}
152+
153+
public function resolve(int|string $id): ?CanAuthenticate
154+
{
155+
$attributes = $this->ldap->findUserByIdentifier($id);
156+
157+
if ($attributes === null) {
158+
return null;
159+
}
160+
161+
return new User(
162+
username: $attributes['uid'] ?? null,
163+
email: $attributes['mail'] ?? null,
164+
displayName: $attributes['cn'] ?? null
165+
);
166+
}
167+
168+
public function resolveId(CanAuthenticate $authenticatable): int|string
78169
{
79-
// …
170+
return $authenticatable->email;
80171
}
81172
}
82173
```
83174

84-
## Built-in user model
175+
To instruct Tempest that you want to use your own resolver, you will need to create a dedicated [initializer](../1-essentials/05-container.md#implementing-an-initializer).
85176

86-
Tempest's authenticator and authorizer are compatible with any class implementing the {`Tempest\Auth\CanAuthenticate`} and {`Tempest\Auth\CanAuthorize`} interfaces. However, Tempest comes with a pre-built `User` model that makes it easier to get started. In order to use Tempest's `User` implementation, you must install the auth files:
177+
```php app/Authentication/LdapAuthenticatableResolverInitializer.php
178+
use Tempest\Auth\Authentication\AuthenticatableResolver;
87179

180+
final class LdapAuthenticatableResolverInitializer implements Initializer
181+
{
182+
#[Singleton]
183+
public function initialize(Container $container): AuthenticatableResolver
184+
{
185+
return new LdapAuthenticatableResolver(
186+
ldap: $container->get(LdapClient::class),
187+
);
188+
}
189+
}
88190
```
89-
./tempest install auth
90-
./tempest migrate:up
191+
192+
### Custom authenticator
193+
194+
By default, Tempest uses the provided {b`Tempest\Auth\Authentication\SessionAuthenticator`} to remember the authenticated model across requests using browser sessions.
195+
196+
However, you may provide your own authenticator by implementing the {b`Tempest\Auth\Authentication\Authenticator`} interface. For instance, may want the model to be authenticated for the duration of the request only.
197+
198+
```php app/Authentication/RequestAuthenticator.php
199+
use Tempest\Auth\Authentication\Authenticator;
200+
use Tempest\Auth\Authentication\CanAuthenticate;
201+
202+
#[Autowire]
203+
final class RequestAuthenticator implements Authenticator
204+
{
205+
private ?CanAuthenticate $current = null;
206+
207+
public function authenticate(CanAuthenticate $authenticatable): void
208+
{
209+
$this->current = $authenticatable;
210+
}
211+
212+
public function deauthenticate(): void
213+
{
214+
$this->current = null;
215+
}
216+
217+
public function current(): ?CanAuthenticate
218+
{
219+
return $this->current;
220+
}
221+
}
91222
```
92223

93-
With this `User` model, you already have a lot of helper methods in place to build your own user management flow:
224+
## Access control
225+
226+
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.
227+
228+
To solve this problem, Tempest provides the ability to write policies. A policy defines the authorization rules for a specific resource, allowing you to implement complex business logic around who can access that resource.
229+
230+
This paradigm is known as [policy-based access control](https://en.wikipedia.org/wiki/Attribute-based_access_control). Policies build on the concept of actions, resources and subjects:
231+
232+
- An action is a specific operation that can be performed on a resource, such as `view`, `edit`, or `delete`.
233+
- A resource may be anything represented by a class.
234+
- A subject is the entity that is trying to perform the action, typically the authenticated user.
235+
236+
### Defining policies
237+
238+
To create a policy, you may define a method in any class and annotate it with the {b`#[Tempest\Auth\AccessControl\PolicyFor]`} attribute. Typically, this is done in a dedicated policy class.
239+
240+
The attribute expects the class name of the resource as its first parameter, and an optional action name as the second parameter. If the action name is not provided, the kebab-cased method name is used instead.
241+
242+
```php app/PostPolicy.php
243+
use Tempest\Auth\AccessControl\PolicyFor;
244+
use Tempest\Auth\AccessControl\AccessDecision;
245+
246+
final class PostPolicy
247+
{
248+
#[PolicyFor(Post::class)]
249+
public function view(Post $post): bool
250+
{
251+
if (! $post->published) {
252+
return false;
253+
}
254+
255+
return true;
256+
}
257+
258+
#[PolicyFor(Post::class, action: ['edit', 'update'])]
259+
public function edit(Post $post, ?User $user): bool
260+
{
261+
if ($user === null) {
262+
return false;
263+
}
264+
265+
return $post->authorId === $user->id->value;
266+
}
267+
}
268+
```
269+
270+
The policy method will be given the resource instance as the first parameter and the subject as the second one. Both of these may be `null`, depending on the context in which the policy is evaluated.
271+
272+
The policy method is expected to return a boolean value or an {b`Tempest\Auth\AccessControl\AccessDecision`} instance. The latter can be used to provide more context about the decision:
273+
274+
```php
275+
return AccessDecision::denied('You must be authenticated to perform this action.');
276+
```
277+
278+
### Checking for permissions
279+
280+
You may inject the {b`Tempest\Auth\AccessControl\AccessControl`} interface to check if a specific action is granted for a resource and subject. Typically, the `denyAccessUnlessGranted()` method is called in a controller.
281+
282+
```php app/Controllers/PostController.php
283+
use Tempest\Auth\AccessControl\AccessControl;
284+
285+
final readonly class PostController
286+
{
287+
public function __construct(
288+
private AccessControl $accessControl,
289+
) {}
290+
291+
#[Delete('/posts/{post}')]
292+
public function delete(Post $post): Redirect
293+
{
294+
$this->accessControl->denyAccessUnlessGranted('delete', $post);
295+
296+
// Proceed with deletion...
297+
298+
return new Redirect('/posts');
299+
}
300+
}
301+
```
302+
303+
Alternatively, you may use the `isGranted()` method. It will return a boolean indicating whether the action is granted for the resource and subject.
304+
305+
:::info
306+
Note that the subject is optional in both methods—if omitted, the [authenticated model](#authentication) is automatically provided.
307+
:::
308+
309+
### Resources without instances
310+
311+
When evaluating the ability to perform an action on a resource without an instance, you may pass the class name of the resource as a string. Typically, this is used when checking if a subject has the permissions to create a new resource.
94312

95313
```php
96-
use App\Auth\User;
97-
98-
$user = new User(
99-
name: 'Brent',
100-
101-
)
102-
->setPassword('password')
103-
->save()
104-
->grantPermission('admin');
314+
$accessControl->isGranted('create', resource: Post::class, subject: $user);
105315
```

0 commit comments

Comments
 (0)