Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Tipos de token y la entidad ApiToken

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.

Vale, ¿y si necesitas permitir el acceso programático a tu API?

Tipos de tokens de acceso

Cuando hablas con una API mediante código, envías un token de API, comúnmente conocido como token de acceso:

fetch('/api/kittens', {
    headers: {
        'Authorization': 'Bearer THE-ACCESS-TOKEN',
    }
});

La forma exacta de obtener ese token varía. Pero hay dos casos principales.

En primer lugar, como usuario del sitio, como un dragón, quieres generar un token de API para poder utilizarlo personalmente en un script que estés escribiendo. Esto es como un token de acceso personal de GitHub. Estos se crean literalmente a través de una interfaz web. Vamos a mostrar esto.

El segundo caso de uso principal es cuando un tercero quiere hacer peticiones a tu API en nombre de un usuario de tu sistema. Por ejemplo, un nuevo sitio llamadoDragonTreasureOrganizer.com quiere hacer una petición a nuestra API en nombre de algunos de nuestros usuarios, por ejemplo, buscar los tesoros de un usuario y mostrarlos artísticamente en su sitio. En esta situación, en lugar de que nuestros usuarios generen tokens manualmente y luego... como... los introduzcan en ese sitio, ofrecerás OAuth. OAuth es básicamente un mecanismo para que los usuarios normales den de forma segura tokens de acceso para su cuenta a un tercero. Y así, tu sitio, o en algún lugar de tu infraestructura tendrás un servidor OAuth.

Eso está fuera del alcance de este tutorial. Pero lo importante es que, una vez hecho el OAuth, el cliente de la API acabará con, lo has adivinado, ¡un token de API! Así que no importa en qué viaje estés, si estás haciendo acceso programático, tus usuarios de la API terminarán con un token de acceso. Y tu trabajo consistirá en leerlo y comprenderlo. Haremos exactamente eso.

¿JWT vs Almacenamiento en Base de Datos?

Como he mencionado, vamos a mostrar un sistema que permite a los usuarios generar sus propios tokens de acceso. ¿Cómo lo hacemos? De nuevo, hay dos formas principales. ¡Muerte por elección!

La primera es generar algo llamado Token Web JSON o JWT. Lo bueno de los JWT es que no necesitan almacenamiento en bases de datos. Son cadenas especiales que en realidad contienen información en su interior. Por ejemplo, puedes crear una cadena JWT que incluya el id de usuario y algunos ámbitos.

Uno de los inconvenientes de los JWT es que no hay una forma fácil de "cerrar sesión"... porque no hay una forma automática de invalidar los JWT. Les das una fecha de caducidad cuando los creas... pero entonces son válidos hasta entonces... pase lo que pase, a menos que añadas alguna complejidad extra... lo que anula un poco el propósito.

Los JWT están de moda, son populares y divertidos Pero... puede que no los necesites. Son geniales cuando tienes un sistema de inicio de sesión único porque, si ese JWT se utiliza para autenticarse con varios sistemas o API, cada API puede validar el JWT por sí misma: sin necesidad de hacer una petición de API a un sistema central de autenticación.

Así que es posible que acabes utilizando JWT, para lo que existe un bundle estupendo llamado LexikJWTAuthenticationBundle. Los JWT son también el tipo de token de acceso que al final te da OpenID.

En lugar de los JWT, la segunda opción principal es muy sencilla: generar una cadena de token aleatoria y almacenarla en la base de datos. Esto también te permite invalidar los tokens de acceso... ¡simplemente borrándolos! Esto es lo que haremos.

Generar la entidad

Así que manos a la obra. Para almacenar los tokens de la API, ¡necesitamos una nueva entidad! Busca tu terminal y ejecuta:

php ./bin/console make:entity

Y llamémosla ApiToken. En teoría, podrías permitir a los usuarios autenticarse a través de un formulario de inicio de sesión o HTTP básico y luego enviar una petición POST para crear tokens de API si quieres... pero no lo haremos.

Añade una propiedad ownedBy. Esto va a ser un ManyToOne a User y no nullable. Y diré "sí" a la inversa. Así que la idea es que cada Userpueda tener muchos tokens de API. Cuando se utiliza un token de API, queremos saber con qué Userestá relacionado. Lo utilizaremos durante la autenticación. Llamar a la propiedadapiTokens está bien y decir no a la eliminación de huérfanos. Siguiente propiedad: expiresAt datetime_immutable y diré que sí a nullable. Tal vez permitamos que los tokens no caduquen nunca dejando este campo en blanco. La siguiente es token, que será una cadena. Voy a establecer la longitud en 68 -veremos por qué en un minuto- y no ennullable. Y por último, añade una propiedad scopes como tipo json. Esto va a ser bastante guay: almacenaremos una matriz de "permisos" que debe tener este token de API. En este caso, tampoco nullable. Pulsa intro para terminar.

Muy bien, gira a tu editor. Sin sorpresas: eso ha creado una entidad ApiToken... y no hay nada muy interesante dentro de ella:

... lines 1 - 2
namespace App\Entity;
use App\Repository\ApiTokenRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ApiTokenRepository::class)]
class ApiToken
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'apiTokens')]
#[ORM\JoinColumn(nullable: false)]
private ?User $ownedBy = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $expiresAt = null;
#[ORM\Column(length: 68)]
private string $token = null;
#[ORM\Column]
private array $scopes = [];
... lines 28 - 80
}

Así que vamos a hacer la migración correspondiente:

symfony console make:migration

Gira y echa un vistazo a ese archivo para asegurarte de que se ve bien. ¡Sí! Crea la tabla api_token:

... lines 1 - 12
final class Version20230209183006 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SEQUENCE api_token_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE api_token (id INT NOT NULL, owned_by_id INT NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, token VARCHAR(68) NOT NULL, scopes JSON NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_7BA2F5EB5E70BCD7 ON api_token (owned_by_id)');
$this->addSql('COMMENT ON COLUMN api_token.expires_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE api_token ADD CONSTRAINT FK_7BA2F5EB5E70BCD7 FOREIGN KEY (owned_by_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP SEQUENCE api_token_id_seq CASCADE');
$this->addSql('ALTER TABLE api_token DROP CONSTRAINT FK_7BA2F5EB5E70BCD7');
$this->addSql('DROP TABLE api_token');
}
}

Ejecuta eso con:

symfony console doctrine:migrations:migrate

Y... ¡genial! A continuación: vamos a añadir una forma de generar la cadena de tokens aleatorios. Luego, hablaremos de ámbitos y cargaremos nuestros accesorios con algunos tokens de la API.

Leave a comment!

6
Login or Register to join the conversation
MildDisaster Avatar
MildDisaster Avatar MildDisaster | posted hace 19 días

I'm curious in this example, why you have potentially one user - many tokens relationship?
Wouldn't one user - one token suffice ?

Reply

Hey @MildDisaster ,

That's how GitHub access tokens, you can create many tokens on GitHub for your account. First of all, you can create different tokens with different scopes, like some tokens may have access to the private repos while others - only public ones. That's the most common and flexible structure I think :) Moreover, you can use one token e.g. for Composer and another token for your website that sends API requests to GitHub. And it's convenient this way, as you can e.g. remove that second token but the first one will still work.

But of course it depends on your personal use case, and if you're happy with OneToOne relation - go for it, there's nothing wrong with it, just some limitations I mentioned above :)

Cheers!

1 Reply
MildDisaster Avatar
MildDisaster Avatar MildDisaster | Victor | posted hace 19 días | edited

I think I understand. So the relationship is more (but not exclusively) between tokens and apps ( that a user will use to access api with ) ?

What is the preferred way to handle tokens once an associated account is changed, ie (password recovered) ?

Should one invalidate existing tokens, or just let them be ?

I noticed in the symfonycasts github there are projects for account registration and password recovery. Without double checking, don't think there was mention of token cleanup in them.

Thanks !

Reply

Hey @MildDisaster ,

I think I understand. So the relationship is more (but not exclusively) between tokens and apps ( that a user will use to access api with ) ?

Mostly yes, I suppose, but once again, it depends on your specific use case. But from my experience it seems more like this :)

What is the preferred way to handle tokens once an associated account is changed, ie (password recovered) ?

Good question. I think it also depends on your specific use case and your strategy. I don't think GitHub invalidate all tokens if you changed password... but in some cases this might be a good idea. It depends on how much your users will be annoyed by it :)

Yeah, we support those bundles. And yeah, probably it's a good use case when you have to clean up password reset tokens on password change :)

Cheers!

1 Reply
Someswara-rao-J Avatar
Someswara-rao-J Avatar Someswara-rao-J | posted hace 4 meses

How to authenticate jwt token string (First Way to make token). I have done react login page. Successfully logged in, But not getting Authenticated.

Reply

Hey @Someswara-rao-J!

Hmm. Did you create an access token authenticator like we did? https://symfonycasts.com/screencast/api-platform-security/access-token-authenticator

If so, here is what I would do to debug:

A) After making the AJAX request to "log in", find the Symfony profiler for that request and open it (reminder: you can always go to /_profiler to see a list of the most recent requests to your site - and you can find the AJAX request you just made there. When you get to the profiler for that request, click on the "Security" tab. Does it show you as authenticated?

B) If it does NOT show you as authenticated, then look more closely at your access token authenticator system: make sure your ApiTokenHandler is being called, etc. If it DOES show that you are authenticated... but then future AJAX requests appear to be not authenticated, it means that you are "losing" authentication between requests.

There are a few things that could cause this:

1) If you have stateless: true on your firewall, this will happen. This is because this tells your firewall to NOT use session/cookie storage.
2) If your frontend and backend are on different subdomains this could happen. For example, if your API is api.example.com and your frontend is just frontend.example.com, by default, Symfony will create a cookie for api.example.com and (iirc) that will not be usable by your frontend. You can adjust your cookie domain in the Symfony settings if this is the cause
3) In some situations, if you've added some custom serialization logic to your User class, you can "lose" authentication on the 2nd request. If you check the logs on the first request after the successful login, you should see something like:

Cannot refresh token because user has changed

Let me know if this helps!

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