Skip to content

Commit b232756

Browse files
docs(jwt): support for laravel and reorganize (#2095)
1 parent 88fd117 commit b232756

File tree

4 files changed

+434
-302
lines changed

4 files changed

+434
-302
lines changed

core/jwt.md

Lines changed: 2 additions & 302 deletions
Original file line numberDiff line numberDiff line change
@@ -8,305 +8,5 @@
88
>
99
> [Wikipedia](https://en.wikipedia.org/wiki/JSON_Web_Token)
1010
11-
API Platform allows to easily add a JWT-based authentication to your API using [LexikJWTAuthenticationBundle](https://github.com/lexik/LexikJWTAuthenticationBundle).
12-
13-
<p align="center" class="symfonycasts"><a href="https://symfonycasts.com/screencast/symfony-rest4/json-web-token?cid=apip"><img src="../symfony/images/symfonycasts-player.png" alt="JWT screencast"><br>Watch the LexikJWTAuthenticationBundle screencast</a></p>
14-
15-
## Installing LexikJWTAuthenticationBundle
16-
17-
We begin by installing the bundle:
18-
19-
```console
20-
composer require lexik/jwt-authentication-bundle
21-
```
22-
Then we need to generate the public and private keys used for signing JWT tokens.
23-
24-
You can generate them by using this command:
25-
26-
```console
27-
php bin/console lexik:jwt:generate-keypair
28-
```
29-
30-
Or if you're using the [API Platform distribution with Symfony](../symfony/index.md), you may run this from the project's root directory:
31-
32-
```console
33-
docker compose exec php sh -c '
34-
set -e
35-
apt-get install openssl
36-
php bin/console lexik:jwt:generate-keypair
37-
setfacl -R -m u:www-data:rX -m u:"$(whoami)":rwX config/jwt
38-
setfacl -dR -m u:www-data:rX -m u:"$(whoami)":rwX config/jwt
39-
'
40-
```
41-
42-
Note that the `setfacl` command relies on the `acl` package. This is installed by default when using the API Platform
43-
docker distribution but may need to be installed in your working environment in order to execute the `setfacl` command.
44-
45-
This takes care of keypair creation (including using the correct passphrase to encrypt the private key), and setting the
46-
correct permissions on the keys allowing the web server to read them.
47-
48-
If you want the keys to be auto generated in `dev` environment, see an example in the
49-
[docker-entrypoint script of api-platform/demo](https://github.com/api-platform/demo/blob/a03ce4fb1f0e072c126e8104e42a938bb840bffc/api/docker/php/docker-entrypoint.sh#L16-L17).
50-
51-
Since these keys are created by the `root` user from a container, your host user will not be able to read them during
52-
the `docker compose build caddy` process. Add the `config/jwt/` folder to the `api/.dockerignore` file so that they are
53-
skipped from the result image.
54-
55-
The keys should not be checked in to the repository (i.e. it's in `api/.gitignore`). However, note that a JWT token could
56-
only pass signature validation against the same pair of keys it was signed with. This is especially relevant in a production
57-
environment, where you don't want to accidentally invalidate all your clients' tokens at every deployment.
58-
59-
For more information, refer to [the bundle's documentation](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst)
60-
or read a [general introduction to JWT here](https://jwt.io/introduction/).
61-
62-
We're not done yet! Let's move on to configuring the Symfony SecurityBundle for JWT authentication.
63-
64-
## Configuring the Symfony SecurityBundle
65-
66-
It is necessary to configure a user provider. You can either use the [Doctrine entity user provider](https://symfony.com/doc/current/security/user_provider.html#entity-user-provider)
67-
provided by Symfony (recommended), [create a custom user provider](https://symfony.com/doc/current/security/user_provider.html#creating-a-custom-user-provider)
68-
or use [API Platform's FOSUserBundle integration](../symfony/fosuser-bundle.md) (not recommended).
69-
70-
If you choose to use the Doctrine entity user provider, start by [creating your `User` class](https://symfony.com/doc/current/security.html#a-create-your-user-class).
71-
72-
Then update the security configuration:
73-
74-
```yaml
75-
# api/config/packages/security.yaml
76-
security:
77-
# https://symfony.com/doc/current/security.html#c-hashing-passwords
78-
password_hashers:
79-
App\Entity\User: 'auto'
80-
81-
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
82-
providers:
83-
# used to reload user from session & other features (e.g. switch_user)
84-
users:
85-
entity:
86-
class: App\Entity\User
87-
property: email
88-
# mongodb:
89-
# class: App\Document\User
90-
# property: email
91-
92-
firewalls:
93-
dev:
94-
pattern: ^/_(profiler|wdt)
95-
security: false
96-
main:
97-
stateless: true
98-
provider: users
99-
json_login:
100-
check_path: auth # The name in routes.yaml is enough for mapping
101-
username_path: email
102-
password_path: password
103-
success_handler: lexik_jwt_authentication.handler.authentication_success
104-
failure_handler: lexik_jwt_authentication.handler.authentication_failure
105-
jwt: ~
106-
107-
access_control:
108-
- { path: ^/$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI
109-
- { path: ^/docs, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI docs
110-
- { path: ^/auth, roles: PUBLIC_ACCESS }
111-
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
112-
```
113-
114-
You must also declare the route used for `/auth`:
115-
116-
```yaml
117-
# api/config/routes.yaml
118-
auth:
119-
path: /auth
120-
methods: ['POST']
121-
```
122-
123-
If you want to avoid loading the `User` entity from database each time a JWT token needs to be authenticated, you may consider using
124-
the [database-less user provider](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/8-jwt-user-provider.rst) provided by LexikJWTAuthenticationBundle. However, it means you will have to fetch the `User` entity from the database yourself as needed (probably through the Doctrine EntityManager).
125-
126-
Refer to the section on [Security](security.md) to learn how to control access to API resources and operations. You may
127-
also want to [configure Swagger UI for JWT authentication](#documenting-the-authentication-mechanism-with-swaggeropen-api).
128-
129-
### Adding Authentication to an API Which Uses a Path Prefix
130-
131-
If your API uses a [path prefix](https://symfony.com/doc/current/routing/external_resources.html#route-groups-and-prefixes), the security configuration would look something like this instead:
132-
133-
```yaml
134-
# api/config/packages/security.yaml
135-
security:
136-
# https://symfony.com/doc/current/security.html#c-hashing-passwords
137-
password_hashers:
138-
App\Entity\User: 'auto'
139-
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
140-
providers:
141-
# used to reload user from session & other features (e.g. switch_user)
142-
users:
143-
entity:
144-
class: App\Entity\User
145-
property: email
146-
147-
firewalls:
148-
dev:
149-
pattern: ^/_(profiler|wdt)
150-
security: false
151-
api:
152-
pattern: ^/api/
153-
stateless: true
154-
provider: users
155-
jwt: ~
156-
main:
157-
json_login:
158-
check_path: auth # The name in routes.yaml is enough for mapping
159-
username_path: email
160-
password_path: password
161-
success_handler: lexik_jwt_authentication.handler.authentication_success
162-
failure_handler: lexik_jwt_authentication.handler.authentication_failure
163-
164-
access_control:
165-
- { path: ^/$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI
166-
- { path: ^/docs, roles: PUBLIC_ACCESS } # Allows accessing API documentations and Swagger UI docs
167-
- { path: ^/auth, roles: PUBLIC_ACCESS }
168-
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
169-
```
170-
171-
### Be sure to have lexik_jwt_authentication configured on your user_identity_field
172-
173-
```yaml
174-
# api/config/packages/lexik_jwt_authentication.yaml
175-
lexik_jwt_authentication:
176-
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
177-
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
178-
pass_phrase: '%env(JWT_PASSPHRASE)%'
179-
```
180-
181-
## Documenting the Authentication Mechanism with Swagger/Open API
182-
183-
Want to test the routes of your JWT-authentication-protected API?
184-
185-
### Configuring API Platform
186-
187-
```yaml
188-
# api/config/packages/api_platform.yaml
189-
api_platform:
190-
swagger:
191-
api_keys:
192-
JWT:
193-
name: Authorization
194-
type: header
195-
```
196-
197-
The "Authorize" button will automatically appear in Swagger UI.
198-
199-
![Screenshot of API Platform with Authorize button](images/JWTAuthorizeButton.png)
200-
201-
### Adding a New API Key
202-
203-
All you have to do is configure the API key in the `value` field.
204-
By default, [only the authorization header mode is enabled](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst#2-use-the-token) in LexikJWTAuthenticationBundle.
205-
You must set the [JWT token](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.rst#1-obtain-the-token) as below and click on the "Authorize" button.
206-
207-
`Bearer MY_NEW_TOKEN`
208-
209-
![Screenshot of API Platform with the configuration API Key](images/JWTConfigureApiKey.png)
210-
211-
### Adding endpoint to SwaggerUI to retrieve a JWT token
212-
213-
LexikJWTAuthenticationBundle has an integration with API Platform to automatically
214-
add an OpenAPI endpoint to conveniently retrieve the token in Swagger UI.
215-
216-
If you need to modify the default configuration, you can do it in the dedicated configuration file:
217-
218-
```yaml
219-
# config/packages/lexik_jwt_authentication.yaml
220-
lexik_jwt_authentication:
221-
# ...
222-
api_platform:
223-
check_path: /auth
224-
username_path: email
225-
password_path: password
226-
```
227-
228-
You will see something like this in Swagger UI:
229-
230-
![API Endpoint to retrieve JWT Token from SwaggerUI](images/jwt-token-swagger-ui.png)
231-
232-
## Testing
233-
234-
To test your authentication with `ApiTestCase`, you can write a method as below:
235-
236-
```php
237-
<?php
238-
// tests/AuthenticationTest.php
239-
240-
namespace App\Tests;
241-
242-
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
243-
use App\Entity\User;
244-
use Hautelook\AliceBundle\PhpUnit\ReloadDatabaseTrait;
245-
246-
class AuthenticationTest extends ApiTestCase
247-
{
248-
use ReloadDatabaseTrait;
249-
250-
public function testLogin(): void
251-
{
252-
$client = self::createClient();
253-
$container = self::getContainer();
254-
255-
$user = new User();
256-
$user->setEmail('test@example.com');
257-
$user->setPassword(
258-
$container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T')
259-
);
260-
261-
$manager = $container->get('doctrine')->getManager();
262-
$manager->persist($user);
263-
$manager->flush();
264-
265-
// retrieve a token
266-
$response = $client->request('POST', '/auth', [
267-
'headers' => ['Content-Type' => 'application/json'],
268-
'json' => [
269-
'email' => 'test@example.com',
270-
'password' => '$3CR3T',
271-
],
272-
]);
273-
274-
$json = $response->toArray();
275-
$this->assertResponseIsSuccessful();
276-
$this->assertArrayHasKey('token', $json);
277-
278-
// test not authorized
279-
$client->request('GET', '/greetings');
280-
$this->assertResponseStatusCodeSame(401);
281-
282-
// test authorized
283-
$client->request('GET', '/greetings', ['auth_bearer' => $json['token']]);
284-
$this->assertResponseIsSuccessful();
285-
}
286-
}
287-
```
288-
289-
Refer to [Testing the API](../symfony/testing.md) for more information about testing API Platform.
290-
291-
### Improving Tests Suite Speed
292-
293-
Since now we have a `JWT` authentication, functional tests require us to log in each time we want to test an API endpoint. This is where [Password Hashers](https://symfony.com/doc/current/security/passwords.html) come into play.
294-
295-
Hashers are used for 2 reasons:
296-
297-
1. To generate a hash for a raw password (`$container->get('security.user_password_hasher')->hashPassword($user, '$3CR3T')`)
298-
2. To verify a password during authentication
299-
300-
While hashing and verifying 1 password is quite a fast operation, doing it hundreds or even thousands of times in a tests suite becomes a bottleneck, because reliable hashing algorithms are slow by their nature.
301-
302-
To significantly improve the test suite speed, we can use more simple password hasher specifically for the `test` environment.
303-
304-
```yaml
305-
# override in api/config/packages/test/security.yaml for test env
306-
security:
307-
password_hashers:
308-
App\Entity\User:
309-
algorithm: md5
310-
encode_as_base64: false
311-
iterations: 0
312-
```
11+
- For Symfony users, check out the [JWT Authentication with Symfony documentation](/symfony/jwt.md).
12+
- For Laravel users, explore the [JWT Authentication with Laravel documentation](/laravel/jwt.md).

0 commit comments

Comments
 (0)