|
8 | 8 | >
|
9 | 9 | > ―[Wikipedia](https://en.wikipedia.org/wiki/JSON_Web_Token)
|
10 | 10 |
|
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 |
| - |
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 |
| - |
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 |
| - |
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