Autenticador de token de acceso
Para autenticarse con un token, un cliente de la API enviará una cabecera Authorization con la palabra Bearer y, a continuación, la cadena del token... que no es más que una práctica estándar:
$client->request('GET', '/api/treasures', [
'headers' => [
'Authorization' => 'Bearer TOKEN',
],
]);
Entonces algo en nuestra aplicación leerá esa cabecera, se asegurará de que el token es válido, autenticará al usuario y montará una gran fiesta para celebrarlo.
Activar access_token
Afortunadamente, ¡Symfony tiene el sistema perfecto para esto! Gira y abreconfig/packages/security.yaml. En cualquier lugar bajo tu cortafuegos añade access_token:
| security: | |
| // ... lines 2 - 11 | |
| firewalls: | |
| // ... lines 13 - 15 | |
| main: | |
| // ... lines 17 - 24 | |
| access_token: | |
| // ... lines 26 - 52 |
Esto activa una escucha que observará cada petición para ver si tiene una cabeceraAuthorization. Si lo tiene, lo leerá e intentará autenticar al usuario.
Sin embargo, requiere una clase ayudante... porque aunque sabe dónde encontrar el token en la petición... ¡no tiene ni idea de qué hacer con él! No sabe si se trata de un JWT que debe descodificar... o, en nuestro caso, que puede consultar la base de datos en busca del registro coincidente. Así que, para ayudarle, añade una opción token_handler establecida en el id de un servicio que crearemos: App\Security\ApiTokenHandler:
| security: | |
| // ... lines 2 - 11 | |
| firewalls: | |
| // ... lines 13 - 15 | |
| main: | |
| // ... lines 17 - 24 | |
| access_token: | |
| token_handler: App\Security\ApiTokenHandler | |
| // ... lines 27 - 52 |
Cortafuegos sin estado
Por cierto, si tu sistema de seguridad sólo permite la autenticación mediante un token de API, entonces no necesitas almacenamiento de sesión. En ese caso, puedes establecer una bandera stateless: true que indique al sistema de seguridad que, cuando un usuario se autentique, no se moleste en almacenar la información del usuario en la sesión. Voy a eliminar eso, porque tenemos una forma de iniciar sesión que depende de la sesión.
La clase Token Handler
Bien, vamos a crear esa clase manejadora. En el directorio src/ crea un nuevo subdirectorio llamado Security/ y dentro de él una nueva clase PHP llamadaApiTokenHandler. Esta es una clase muy sencilla. Haz que implementeAccessTokenHandlerInterface y luego ve a "Código"->"Generar" o Command+N en un Mac y selecciona "Implementar Métodos" para generar el que necesitamos:getUserBadgeFrom():
| // ... lines 1 - 2 | |
| namespace App\Security; | |
| use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; | |
| use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |
| class ApiTokenHandler implements AccessTokenHandlerInterface | |
| { | |
| public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge | |
| { | |
| // TODO: Implement getUserBadgeFrom() method. | |
| } | |
| } |
El sistema access_token sabe cómo encontrar el token: sabe que vivirá en una cabecera Authorization con la palabra Bearer delante. Así que coge esa cadena, llama a getUserBadgeFrom() y nos la pasa. Por cierto, este atributo#[\SensitiveParameter] es una nueva característica de PHP. Está bien, pero no es importante: sólo asegura que si se lanza una excepción, este valor no se mostrará en el stacktrace.
Nuestro trabajo aquí es consultar la base de datos utilizando el $accessToken y luego devolver a qué usuario se refiere. Para ello, ¡necesitamos el ApiTokenRepository! Añade un método construct con un argumento private ApiTokenRepository $apiTokenRepository:
| // ... lines 1 - 4 | |
| use App\Repository\ApiTokenRepository; | |
| // ... lines 6 - 9 | |
| class ApiTokenHandler implements AccessTokenHandlerInterface | |
| { | |
| public function __construct(private ApiTokenRepository $apiTokenRepository) | |
| { | |
| } | |
| // ... lines 15 - 25 | |
| } |
Abajo, digamos $token = $this->apiTokenRepository y luego llama a findOneBy()pasándole un array, para que consulte donde el campo token es igual a $accessToken:
| // ... lines 1 - 9 | |
| class ApiTokenHandler implements AccessTokenHandlerInterface | |
| { | |
| // ... lines 12 - 15 | |
| public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge | |
| { | |
| $token = $this->apiTokenRepository->findOneBy(['token' => $accessToken]); | |
| // ... lines 19 - 24 | |
| } | |
| } |
Si la autenticación falla por cualquier motivo, necesitamos lanzar un tipo de excepción de seguridad. Por ejemplo, si el token no existe, lanzar una nuevaBadCredentialsException: la de los componentes Symfony:
| // ... lines 1 - 5 | |
| use Symfony\Component\Security\Core\Exception\BadCredentialsException; | |
| // ... lines 7 - 9 | |
| class ApiTokenHandler implements AccessTokenHandlerInterface | |
| { | |
| // ... lines 12 - 15 | |
| public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge | |
| { | |
| $token = $this->apiTokenRepository->findOneBy(['token' => $accessToken]); | |
| if (!$token) { | |
| throw new BadCredentialsException(); | |
| } | |
| // ... lines 23 - 24 | |
| } | |
| } |
Esto hará que falle la autenticación... pero no necesitamos pasar un mensaje. Esto devolverá un mensaje "Credenciales incorrectas." al usuario.
Llegados a este punto, hemos encontrado la entidad ApiToken. Pero, en última instancia, nuestro sistema de seguridad quiere autenticar a un usuario... no un "Token API". Lo hacemos devolviendo un UserBadge que, en cierto modo, envuelve al objeto User. Observa: devuelve un new UserBadge(). El primer argumento es el "identificador de usuario". Pasa $token->getOwnedBy() para obtener elUser y luego ->getUserIdentifier():
| // ... lines 1 - 7 | |
| use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |
| class ApiTokenHandler implements AccessTokenHandlerInterface | |
| { | |
| // ... lines 12 - 15 | |
| public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge | |
| { | |
| // ... lines 18 - 23 | |
| return new UserBadge($token->getOwnedBy()->getUserIdentifier()); | |
| } | |
| } |
Cómo se carga el objeto usuario
Observa que en realidad no estamos devolviendo el objeto User. Esto se debe principalmente a que... ¡no lo necesitamos! Deja que te lo explique. Mantén pulsado Command o Ctrl y haz clic engetUserIdentifier(). Lo que esto devuelve realmente es el email del usuario . Así que estamos devolviendo un UserBadge con el email del usuario dentro. Lo que ocurre a continuación es lo mismo que ocurre cuando enviamos un email al punto final de autenticación json_login. El sistema de seguridad de Symfony toma ese correo electrónico y, como tenemos este proveedor de usuario, sabe que debe consultar la base de datos en busca de un User con ese email.
Así que volverá a consultar la base de datos en busca del User a través del correo electrónico... lo cual es un poco innecesario, pero está bien. Si quieres evitarlo, podrías pasar un callable al segundo argumento y devolver $token->getOwnedBy(). Pero esto funcionará bien tal como está.
Ah, ¡y probablemente sea buena idea comprobar y asegurarnos de que el token es válido! Si no lo es$token->isValid(), entonces podríamos lanzar otro BadCredentialsException. Pero si quieres personalizar el mensaje, también puedes lanzar un nuevoCustomUserMessageAuthenticationException con "Token caducado" para devolver ese mensaje al usuario:
| // ... lines 1 - 6 | |
| use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; | |
| // ... lines 8 - 10 | |
| class ApiTokenHandler implements AccessTokenHandlerInterface | |
| { | |
| // ... lines 13 - 16 | |
| public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge | |
| { | |
| // ... lines 19 - 24 | |
| if (!$token->isValid()) { | |
| throw new CustomUserMessageAuthenticationException('Token expired'); | |
| } | |
| return new UserBadge($token->getOwnedBy()->getUserIdentifier()); | |
| } | |
| } |
¿Usar el Token en Swagger?
Y... ¡listo! Entonces... ¿cómo probamos esto? Bueno, lo ideal sería probarlo en nuestros documentos Swagger. Voy a abrir una nueva pestaña... y luego cerraré la sesión. Pero mantendré abierta mi pestaña original... ¡así podré robar estos tokens válidos!
Dirígete a los documentos de la API. ¿Cómo podemos decirle a esta interfaz que envíe un token de API cuando haga las peticiones? Bueno, habrás notado que hay un botón "Autorizar". Pero cuando lo pulsamos... ¡está vacío! Eso es porque todavía no le hemos dicho a Open API cómo pueden autenticarse los usuarios. Afortunadamente, podemos hacerlo a través de API Platform.
Abre config/packages/api_platform.yaml. Y una nueva clave llamada swagger, aunque en realidad estamos configurando los documentos de OpenAPI. Para añadir una nueva forma de autenticación, configura api_keys para activar ese tipo, luego access_token... que puede ser lo que quieras. Debajo de esto, dale un nombre a este mecanismo de autenticación... y type: header porque queremos pasar el token como cabecera:
| api_platform: | |
| // ... lines 2 - 7 | |
| swagger: | |
| api_keys: | |
| access_token: | |
| name: Authorization | |
| type: header | |
| // ... lines 13 - 18 |
Esto le dirá a Swagger -a través de nuestros documentos OpenAPI- que podemos enviar tokens de API a través de la cabecera Authorization. Ahora, cuando pulsemos el botón "Autorizar"... ¡sí! Dice "Nombre: Autorización", "En cabecera".
Para usar esto, tenemos que empezar con la palabra Bearer y luego un espacio... porque no lo rellena por nosotros. Hablaremos de ello más adelante. Probemos primero con un token no válido. Pulsa "Autorizar". En realidad, aún no se ha realizado ninguna petición: sólo se ha almacenado el código en JavaScript.
Probemos con la ruta get treasure collection. Cuando ejecutamos... ¡impresionante! ¡A 401! No necesitamos autenticarnos para utilizar este punto final, pero como pasamos una cabecera Authorization con Bearer y luego un token, el nuevo sistema access_tokenlo captó, pasó la cadena a nuestro manejador... pero luego no pudimos encontrar un token coincidente en la base de datos, así que lanzamos el error BadCredentialsException
Puedes verlo aquí abajo: la API devolvió una respuesta vacía, pero con una cabecera que contenía invalid_token y error_description: "Credenciales no válidas".
Comprobación de que la autenticación por token funciona
Así que el caso malo funciona. ¡Probemos el caso feliz! En la otra pestaña, copia uno de los tokens válidos. Vuelve a deslizarte hacia arriba, pulsa "Autorizar" y luego "Cerrar sesión". Cerrar sesión sólo significa que "olvida" el token de la API que hemos establecido hace un minuto. Vuelve a escribir Bearer , pega, pulsa "Autorizar", cierra... y bajemos a probar de nuevo esta ruta. Y... ¡woohoo! ¡A 200!
Así que parece que ha funcionado... ¿pero cómo podemos saberlo? Pues bien, abajo, en la barra de herramientas de depuración web, haz clic para abrir el perfilador de esa petición. En la pestaña Seguridad... ¡sí! Hemos iniciado sesión como Bernie. ¡Éxito!
Lo único que no me gusta es tener que escribir esa cadena Bearer en el cuadro de autorización. No es muy fácil de usar. Así que, a continuación, vamos a solucionarlo aprendiendo cómo podemos personalizar el documento de especificaciones OpenAPI que utiliza Swagger.
19 Comments
Hello machines! After finishing the course I'm trying to add the authenticator to coexist with another one I already had in the project where I'm trying to implement the api. But it seems that something doesn't work, well
This is the security.yaml file:
If I don't put ROLE requirement in api resource, it makes the query and returns fine, but when I put for example that I need ROLE_ADMIN, or any other role, the response is a code 200 and returns the login template that my project has, in this case the LoginAuthenticator.
How can I make these two logins coexist?
That the LoginAuthenticator that my project already had should use sessions and the API one should not use them with
stateless: trueand use the ApiTokenHandler that was made in the course.As far as I'm following the course right now it works fine except for one detail.
When I put in the authentication ‘Bearer asasassa’ the invalid key error appears perfectly, and if I put a correct Bearer it makes the request correctly, but if I don't put any Bearer it also returns the data.
But if I don't set any Bearer it returns the data too, shouldn't it say that there is no api key or that it is not correct?
Hey @Rodrypaladin!
That's actually a great question! When you send with no Bearer, to Symfony, this simply looks like a request that is not trying to send authentication info. The token isn't wrong, there just is no token. The result is that the request is basically "anonymous" / not logged in. If the endpoint that's reached does not require auth, the endpoint will return the real data. If the endpoint does require auth, then it should reject the request with a 401 Unauthorized.
Let me know if that clarifies.
Cheers!
I see this error:
and it works if we do
stateless: true,so in Symfony 7, is this how it works ?
When I authorize using token ( I'm using Symfony 7 ), then it throws following error, but can't we just run session app and API at the same time in Symfony like you're doing in this lecture ?
Hey @sujal_k!
Sorry for the slow reply! To make sure I understand: you need to run a normal session-based security app and a stateless, API token version at the same time? Can you explain the requirements a bit more, that'll help me with a better answer.
Cheers! And sorry to not offer any useful info yet!
When I log out and try to use the token to log in in the authorization button. It doesn't log me in, when I go to security in the profiler, it says "There is no security token.". Even if the token is not valid, I still get a 200 with the data.
I think I followed the steps correctly, any ideas?
Heyy!!
I have the same problem,
i did the same config, and i'm alawys getting a 200 response (with valid or not token plus without Authorization header param)
But when i comment the lazy config i still get a 200 if not Authorization passed in header, and 401 if no valid token
Hi, I experience the same behavior. It seems something is missing in the video...
Can anyone help, please?
@MolloKhan ?
Hey @Twigsee
Sorry for my late reply. I have the same question as here https://symfonycasts.com/screencast/api-platform-security/access-token-authenticator#comment-31679
I believe your authenticator might be misconfigured, let's debug that first
Cheers!
Hi @MolloKhan
I am using Caddy, and having the same issue with loggin in,
When I try to authenticate/ login,
I get a "There is no security token." in my profiler >> security
Does "SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1" this work for Caddy users as well,
I dont know where to put this as I dont have .htaccess file
Hey @Teddy-B
I've not worked with Caddy yet so I don't know how to make it work. Have you asked ChatGPT or any other AI? You may be surprised of how good they are for this kind of problem
Cheers!
Thanks I have heard of ChatGPT never thozght it was something I really wanted to use, but I was indeed impressed with the ansers, I havent solved the issue yet, but I think I am close, I guess I just ask around at stackoverflow then.
Thanks for you quick reply Though!
I figured out I just had to add Content-Type: application -json on the headers and got it working... cant believe it
Ha! So it was not the web server. I thought ApiPlatform adds that header automatically for you but I think Symfony short-circuits the workflow when you try to log in
Thanks for sharing!
I think becaue I am using CORS, intergrating 2 appliciations. I got docker setup and the frontend (React) is seperated from the Backend (Symsony)
Hey @ToNy
Can you see in the profiler the security process kicking in? I think your authenticator is not been called for some reason.
I had my apache misconfigured. I solved with the (in)famous:
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1Thanks for your reply.
Hi !
This example shows how to get User when JWT TOKENs are stored in database.
I'm using LexikJWTAuthenticationBundle. Users are not stored in database.
I want to retrieve Connected User when making request with the generated TOKEN.
How can i do this with class ApiTokenHandler ?
Please help me ! Thanks You
Hi @Ange-B,
Can you give more context? Where exactly do you want to get User info?
IIRC if you are using
LexikJWTAuthenticationBundleit already stores the user info in symfony security system, so it can be easily accessed from anywhere, all you need isloveautowireSecurityclass from Security bundle and use$this->security->getUser()method to get current authenticated userPS maybe I'm missing something so waiting for your feedback
Cheers!
"Houston: no signs of life"
Start the conversation!