Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

¿Token API? ¿Cookies de sesión?

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Acompáñame mientras contamos una historia tan antigua como... la Internet moderna: La autenticación de API. Un tema de bombo y platillo, complejidad y héroes improbables. Los personajes incluyen sesiones, tokens de API, OAuth, ¡tokens web JSON! Pero, ¿qué necesitamos para nuestra situación?

Lo primero que quiero que te preguntes es

¿Quién va a utilizar mi API?

¿Es tu propio JavaScript, o necesitas permitir el acceso programático? ¿Como si alguien fuera a escribir un script que utilizara tu API?

Vamos a repasar estos dos casos de uso... y cada uno tiene algunas complejidades adicionales que discutiremos por el camino.

¡Todo es un Token!

Por cierto, cuando piensas en la autenticación de una API, sueles pensar en un token de API, ¡y es cierto! Pero resulta que... prácticamente toda la autenticación se realiza mediante algún tipo de token. Incluso la autenticación basada en sesión se realiza enviando una cookie... que contiene un único, lo has adivinado, "token". Es una cadena aleatoria que PHP utiliza para encontrar y cargar los datos de sesión relacionados en el servidor.

Así que el truco está en averiguar qué tipo de token necesitas en cada situación y cómo lo obtendrá el usuario final.

Caso 1: Construir para tu propio JavaScript

Hablemos del primer caso de uso: el usuario de tu API es tu propio JavaScript.

Bien, antes de que nos sumerjamos en la seguridad, asegúrate de que tu frontend y tu API viven en el mismo dominio... exactamente en el mismo dominio, no sólo en un subdominio. ¿Por qué? Porque si viven en dos dominios o subdominios diferentes, tendrás que lidiar con CORS: Intercambio de Recursos entre Orígenes.

CORS no sólo añade complejidad a tu configuración, sino que también perjudica al rendimiento. Kévin Dunglas -el desarrollador principal de API Platform- tiene una entrada de blog sobre esto. Incluso muestra una estrategia en la que tu frontend y tu backend pueden vivir en directorios o repositorios totalmente distintos, pero seguir viviendo en el mismo dominio gracias a algunos trucos del servidor web.

Si, por alguna razón, decides poner tu API y tu frontend en subdominios diferentes, entonces tendrás que preocuparte de las cabeceras CORS y puedes solucionarlo con NelmioCorsBundle. Pero no te lo recomiendo.

El caso de las Sesiones

De todos modos, volvamos a la seguridad. Si estás llamando a tu API desde tu propio JavaScript, es probable que el usuario se esté registrando a través de un formulario de acceso con un correo electrónico y una contraseña. No importa si se trata de un formulario de inicio de sesión tradicional o de uno creado con un sofisticado framework JavaScript que se envía mediante AJAX.

Y, sinceramente, una forma muy sencilla de gestionar este caso de uso no es con tokens de API, sino con la autenticación básica HTTP de toda la vida. Es decir, pasando literalmente el correo electrónico y la contraseña a cada ruta. Por ejemplo, el usuario introduce su correo electrónico y contraseña, tú haces una petición API a algún punto final sólo para asegurarte de que es válido, luego almacenas ese correo electrónico y contraseña en JavaScript y lo envías en cada petición API que se realice en adelante. Tu correo electrónico y contraseña funcionan básicamente como un token API.

Sin embargo, esto tiene algunos retos prácticos, como la cuestión de dónde almacenas de forma segura el correo electrónico y la contraseña en JavaScript para poder utilizarlos continuamente. En realidad, éste es un problema en general con JavaScript y las "credenciales", incluidos los tokens de API: tienes que tener mucho cuidado con dónde los almacenas para que otro JavaScript de tu página no pueda leerlos. Hay soluciones: https://bit.ly/auth0-token-storage - pero añade una complejidad que muy probablemente no necesites.

Así que en su lugar, para tu propio JavaScript, puedes utilizar una sesión. Cuando inicias una sesión en Symfony, devuelve una cookie "sólo HTTP"... y esa cookie contiene el id de sesión. Aunque, el contenido de la cookie no es realmente importante: puede ser el id de sesión o algún tipo de token que hayas inventado y estés leyendo en Symfony. Lo realmente importante es que, como la cookie es "sólo HTTP", no puede ser leída por JavaScript: ni por tu JavaScript ni por el de nadie. Pero siempre que realices una petición a la API de tu dominio, esa cookie vendrá con ella... y tu aplicación la utilizará para iniciar la sesión del usuario.

Así que el token de la API en esta situación es simplemente el "identificador de sesión", que se almacena de forma segura en una cookie sólo HTTP. Mmmm. Vamos a codificar este caso de uso.

Ah, y por cierto, un caso extremo en esta situación es si tienes una situación de Inicio de Sesión Único - un SSO. En ese caso, te autenticarás con tu SSO como una aplicación web normal. Cuando termines, tendrás un token, que puedes utilizar para autenticar al usuario con una sesión normal... o puedes utilizar ese token directamente desde tu JavaScript. Se trata de un caso de uso más avanzado que no trataremos en este tutorial... aunque sí hablaremos de cómo leer y validar los tokens de la API, independientemente de su procedencia.

Caso de uso 2: Acceso programático y tokens de API

El segundo gran caso de uso de la autenticación es el acceso programático. Algún código hablará con tu API... además de JavaScript desde dentro del navegador.

En este caso, los clientes de la API enviarán absolutamente algún tipo de cadena de token de la API, por lo que tienes que hacer que tu API pueda leer un token que se envía en cada petición, normalmente en una cabecera Authorization:

$response = $thhpClient->request(
    'GET',
    '/api/treasures',
    [
        'Authorization' => 'Bearer '.$apiToken,
    ],
);

Cómo obtiene el usuario este token depende: hay dos casos principales. El primero es el caso del "token de acceso personal a GitHub". En este caso, un usuario puede ir a una página de tu sitio y hacer clic para crear un nuevo token de acceso. Luego puede copiarlo y utilizarlo en algún código.

El segundo gran caso es OAuth, que no es más que una forma elegante y segura de obtener un token de acceso. Es especialmente importante cuando el "código" que realiza las peticiones a la API lo hace en "nombre" de algún usuario de tu sistema.

Por ejemplo, imagina un sitio -R ReplyToAllCommentsWithHearts.com- que te permite conectarte con GitHub. Una vez lo hayas hecho, ese sitio puede hacer peticiones de API a GitHub para tu cuenta, como hacer comentarios como tu usuario. O imagina una aplicación para iPhone en la que, para iniciar sesión, muestres al usuario el formulario de inicio de sesión de tu sitio. Entonces, a través de un flujo OAuth, esa aplicación móvil recibirá un token de acceso que podrá utilizar para hablar con tu API en nombre de ese usuario.

En este tutorial vamos a hablar del método del token de acceso personal, incluyendo cómo leer y validar los tokens de la API, vengan de donde vengan. No hablaremos del flujo OAuth... y en parte es porque es una bestia aparte. Sí, si tienes un caso de uso en el que necesitas permitir que terceros obtengan tokens de API para diferentes usuarios de tu sitio, necesitarás algún tipo de servidor OAuth, tanto si lo construyes tú mismo como si utilizas alguna otra solución. Pero una vez que el servidor OAuth ha hecho su trabajo, el cliente que hablará con tu API recibe... ¡un token! Y luego utilizarán ese token para hablar con tu API. Así que tu API tendrá que leer, validar y entender ese token, pero no le importa cómo lo obtuvo el cliente de la API.

Bien, dejemos atrás toda esta teoría y empecemos a repasar a continuación el primer caso de uso: permitir que nuestro JavaScript inicie sesión enviando una petición AJAX.

Leave a comment!

7
Login or Register to join the conversation
Pierre-A Avatar
Pierre-A Avatar Pierre-A | posted hace 4 meses

Hello Ryan, another little typo error in your code here :

$response = $thhpClient->request( // This should be httpClient !!!
    'GET',
    '/api/treasures',
    [
        'Authorization' => 'Bearer '.$apiToken,
    ],
);

or someone invented a new protocole: thhp i didn't know ? ;) ... many thanks for tutos !

Reply

Lol - thank you @Pierre-A

Reply
Romain-L Avatar
Romain-L Avatar Romain-L | posted hace 4 meses

Hello Ryan,
Thanks for this tutorial.
Like Ugo, I am also a bit confused but by another another point.
When you say "Or imagine an iPhone app where, to log in, you show the user the login form on your site. Then, via an OAuth flow, that mobile app will receive an access token it can use to talk to your API on behalf of that user."
Why do we need OAuth here ?
They state the same thing on the link you gave (auth0.net) : "If the Application is a native app, then use the Authorization Code Flow with Proof Key for Code Exchange (PKCE)."

I was thinking it was possible to just use JWT to authenticate for a native app. For example, let's take Flutter, we can use the secure storage (https://pub.dev/packages/flutter_secure_storage) to store the JWT, right ? So is there something wrong with sending my email+password to my api endpoint (like in normal spa), then read the answer, store the token in the safe place and then re-use for latter requests by adding this token to the Authorization header (i saw this implementation in few places also). I can also use this token, to display connected / not connected page depending if token is valid or not (by checking from time to time).

But since it's seem mandatory to use Oauth with PKCE, i think i'm missing something about the security but i don't see what :-)
If you can help on this subject, it would be very nice.
Thanks a lot.

Reply

Hey @Romain-L!

Sorry for the slow reply - just got back from vacation :).

Yea.. this stuff is SO tricky - and I'm not an expert on every aspect for sure!

I was thinking it was possible to just use JWT to authenticate for a native app. For example, let's take Flutter, we can use the secure storage (https://pub.dev/packages/flutter_secure_storage) to store the JWT, right ?

Yes, I'm 99% sure that this will "work". And, about storage, I don't know what the normal solution is for this, but even with OAuth, after you complete the process, you'll need to store the access token somewhere. So storing an access token or JWT is a legitimate thing to do.

So is there something wrong with sending my email+password to my api endpoint (like in normal spa), then read the answer, store the token in the safe place and then re-use for latter requests by adding this token to the Authorization header (i saw this implementation in few places also).

I'm not an expert here, but yes, I think this is "fine". What you're seeing is that OAuth is recommended because it's a standard, so there's less risk that you mess something up. Additionally, with OAuth, the user ultimately enters their email/password into a browser with your site's URL in the address bar. With your solution, you enter it directly into the app... which can be a risk (assuming your users are smart enough to notice) because a "bad user" could create an app that looks like your app with a login form... which then sends their credentials somewhere else. So by using OAuth, you can increase the user's trust. But in reality, I'm not sure how many users will actually do this.

Here's another document from Auth0 that talks in more depth about this topic - https://auth0.com/docs/get-started/authentication-and-authorization-flow/mobile-device-login-flow-best-practices

Cheers!

1 Reply
Romain-L Avatar

Hello Ryan,
Thank you very much for your time and your answer. It's more clear to me know. So i will go with the "basic" solution since it's seems to be ok. I checked a few mobile apps and they all allow to enter email/pwd in the app without oauth for there own users, so let's go for this!
Thanks and have a nice day

Reply
Ugo Avatar

Hi Ryan,
I am getting a bit confused..
I am developing a backoffice+api in Symfony and a front end app in vuejs/nuxt. The front end app will be used only by external users that will have to login to get their own data (authentication + authorisation). These two apps have separate repo and I was planning to use two different subdomains (ex: backoffice.myurl.com and myurl.com) and sessions. I am now getting confused with what you explained about CORS. Is anything wrong with my setup ?
thx for your help

Reply

Hi Ugo!

Excellent question! Because you're using sessions, there are 2 things to think about:

1) Can your frontend and backend share a session? The answer is yes. And it will likely happen automatically. When you send a POST request to myurl.com (your API) to authenticate, and then your API creates a session, it will probably create the SameSite session cookie under the domain myurl.com. Because backoffice.myurl.com is a subdomain of that, it can use that cookie. So, all good here ✅

2) Can your frontend JavaScript make Ajax requests to your API? The answer is "yes", "but". You WILL be able to do this. However, your frontend will first make a CORS preflight request basically asking your API if it's ok if backoffice.myurl.com makes Ajax calls to it. So, to get this to work, you'll need to configure some CORS config on your API to say that requests coming from backoffice.myurl.com are "allowed". But, you'll STILL have s flight performance impact because your frontend will first need to make that extra "preflight" request before it makes the real AJAX request. I'm not an expert on CORS, but it looks like you can set a header - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age - so that this preflight request is not made often.

So, CORS is the problem. Your setup will work, but you'll need some CORS config and you'll suffer (slightly) from that preflight request. The alternative, which Kévin Dunglas talks about - https://dunglas.dev/2022/01/preventing-cors-preflight-requests-using-content-negotiation/ - is to just use something like myurl.com/backoffice and then configure your webserver to serve the /backoffice URL from the totally different frontend app code :).

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0
    }
}
userVoice