Generar el token de la API y los accesorios
La propiedad más importante de ApiToken
es la cadena del token... que tiene que ser algo aleatorio. Crea un método constructor con un argumento string $tokenType
:
// ... lines 1 - 8 | |
class ApiToken | |
{ | |
// ... lines 11 - 30 | |
public function __construct(string $tokenType = self::PERSONAL_ACCESS_TOKEN_PREFIX) | |
{ | |
// ... line 33 | |
} | |
// ... lines 35 - 87 | |
} |
Esto no es obligatorio, pero GitHub ha dado con algo ingenioso: como tienen diferentes tipos de tokens, como tokens de acceso personal y tokens OAuth, dan a cada tipo de token su propio prefijo. Esto ayuda a saber de dónde viene cada uno.
Nosotros sólo vamos a tener un tipo, pero seguiremos la idea. En la parte superior, para almacenar el prefijo del tipo, añade private const PERSONAL_ACCESS_TOKEN_PREFIX = 'tcp_'
:
// ... lines 1 - 8 | |
class ApiToken | |
{ | |
private const PERSONAL_ACCESS_TOKEN_PREFIX = 'tcp_'; | |
// ... lines 12 - 87 | |
} |
Yo... me acabo de inventar ese prefijo. Nuestro sitio se llama Treasure Connect... y éste es un token de acceso personal, así que tcp_
.
Abajo, para string $tokenType =
pon por defectoself::PERSONAL_ACCESS_TOKEN_PREFIX
:
// ... lines 1 - 8 | |
class ApiToken | |
{ | |
// ... lines 11 - 30 | |
public function __construct(string $tokenType = self::PERSONAL_ACCESS_TOKEN_PREFIX) | |
{ | |
// ... line 33 | |
} | |
// ... lines 35 - 87 | |
} |
Para el token en sí, digamos $this->token = $tokenType.
y luego usaré un código que generará una cadena aleatoria de 64 caracteres:
// ... lines 1 - 8 | |
class ApiToken | |
{ | |
// ... lines 11 - 30 | |
public function __construct(string $tokenType = self::PERSONAL_ACCESS_TOKEN_PREFIX) | |
{ | |
$this->token = $tokenType.bin2hex(random_bytes(32)); | |
} | |
// ... lines 35 - 87 | |
} |
Así que aquí hay 64 caracteres más el prefijo de 4 caracteres, igual a 68. Por eso he elegido esa longitud. Y como estamos configurando el $token
en el constructor, esto ya no necesita = null
ni ser anulable. Siempre será un string
.
Configurar los accesorios
¡Vale! ¡Ya está configurado! Así que vamos a añadir algunos tokens API a la base de datos. En tu terminal, ejecuta
php ./bin/console make:factory
para que podamos generar una fábrica Foundry para ApiToken
. Ve a ver la nueva clase:src/Factory/ApiTokenFactory.php
. Abajo en getDefaults()
:
// ... lines 1 - 29 | |
final class ApiTokenFactory extends ModelFactory | |
{ | |
// ... lines 32 - 46 | |
protected function getDefaults(): array | |
{ | |
return [ | |
'ownedBy' => UserFactory::new(), | |
'scopes' => [], | |
'token' => self::faker()->text(64), | |
]; | |
} | |
// ... lines 55 - 69 | |
} |
Esto se ve bien en su mayor parte, aunque no necesitamos pasar el token
. Ah, y quiero retocar los ámbitos:
// ... lines 1 - 29 | |
final class ApiTokenFactory extends ModelFactory | |
{ | |
// ... lines 32 - 46 | |
protected function getDefaults(): array | |
{ | |
return [ | |
'ownedBy' => UserFactory::new(), | |
'scopes' => [ | |
// ... lines 52 - 53 | |
], | |
]; | |
} | |
// ... lines 57 - 71 | |
} |
Normalmente, cuando creas un token de acceso -ya sea un token de acceso personal o uno creado a través de OAuth- puedes elegir qué permisos tendrá ese token: no tiene automáticamente todos los permisos que tendría un usuario normal. También quiero añadir eso a nuestro sistema.
De vuelta a ApiToken
, en la parte superior, después de la primera constante, pegaré algunas más:
// ... lines 1 - 8 | |
class ApiToken | |
{ | |
// ... lines 11 - 12 | |
public const SCOPE_USER_EDIT = 'ROLE_USER_EDIT'; | |
public const SCOPE_TREASURE_CREATE = 'ROLE_TREASURE_CREATE'; | |
public const SCOPE_TREASURE_EDIT = 'ROLE_TREASURE_EDIT'; | |
// ... lines 16 - 97 | |
} |
Esto define tres ámbitos diferentes que puede tener un token. No son todos los ámbitos que podríamos imaginar, pero son suficientes para que las cosas sean realistas. Así, cuando creas un token, puedes elegir si ese token debe tener permiso para editar los datos del usuario, o si puede crear tesoros en nombre del usuario o si puede editar tesoros en nombre del usuario. También he añadido un public const SCOPES
para describirlos:
// ... lines 1 - 8 | |
class ApiToken | |
{ | |
// ... lines 11 - 16 | |
public const SCOPES = [ | |
self::SCOPE_USER_EDIT => 'Edit User', | |
self::SCOPE_TREASURE_CREATE => 'Create Treasures', | |
self::SCOPE_TREASURE_EDIT => 'Edit Treasures', | |
]; | |
// ... lines 22 - 97 | |
} |
Volviendo a nuestro ApiTokenFactory
, vamos a dar, por defecto, a cada ApiToken
dos de esos tres ámbitos:
// ... lines 1 - 29 | |
final class ApiTokenFactory extends ModelFactory | |
{ | |
// ... lines 32 - 46 | |
protected function getDefaults(): array | |
{ | |
return [ | |
'ownedBy' => UserFactory::new(), | |
'scopes' => [ | |
ApiToken::SCOPE_TREASURE_CREATE, | |
ApiToken::SCOPE_USER_EDIT, | |
], | |
]; | |
} | |
// ... lines 57 - 71 | |
} |
¡Bien! ApiTokenFactory
está listo. Último paso: abre AppFixtures
para que podamos crear algunos ámbitos ApiToken
. Quiero asegurarme de que, en nuestros datos ficticios, cada usuario tiene al menos uno o dos tokens de API. Una forma fácil de hacerlo, aquí abajo, es decir ApiTokenFactory::createMany()
. Como tenemos 10 usuarios, vamos a crear 30 tokens. Luego le pasamos una función de devolución de llamada y, dentro, devolvemos una anulación de los datos por defecto. Vamos a anular ownedBy
para que sea UserFactory::random()
:
// ... lines 1 - 4 | |
use App\Factory\ApiTokenFactory; | |
// ... lines 6 - 10 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager): void | |
{ | |
// ... lines 15 - 26 | |
ApiTokenFactory::createMany(30, function () { | |
return [ | |
'ownedBy' => UserFactory::random(), | |
]; | |
}); | |
} | |
} |
Esto creará 30 tokens y los asignará aleatoriamente a los 10, bueno en realidad 11, usuarios de la base de datos. Así que, de media, cada usuario debería tener asignados unos tres tokens API. Hago esto porque, para simplificar las cosas, no vamos a crear una interfaz de usuario en la que el usuario pueda hacer clic y crear tokens de acceso y seleccionar ámbitos. Vamos a saltarnos todo eso. En lugar de eso, como cada usuario ya tendrá algunos tokens de la API en la base de datos, podemos pasar directamente a aprender a leer y validar esos tokens.
Recarga los accesorios con:
symfony console doctrine:fixtures:load
Mostrar los tokens en el frontend
Y... ¡precioso! Pero ya que no vamos a construir una interfaz para crear tokens, al menos necesitamos una forma fácil de ver los tokens de un usuario... para poder probarlos en nuestra API. Cuando estemos autenticados, podemos mostrarlos aquí.
No es un detalle muy importante, así que lo haré rápidamente. En User
, en la parte inferior, pegaré una función que devuelva una matriz de las cadenas de token de API válidas para este usuario:
// ... lines 1 - 38 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
// ... lines 41 - 222 | |
/** | |
* @return string[] | |
*/ | |
public function getValidTokenStrings(): array | |
{ | |
return $this->getApiTokens() | |
->filter(fn (ApiToken $token) => $token->isValid()) | |
->map(fn (ApiToken $token) => $token->getToken()) | |
->toArray() | |
; | |
} | |
} |
En ApiToken
, también necesitamos un método isValid()
... así que también lo pegaré:
// ... lines 1 - 8 | |
class ApiToken | |
{ | |
// ... lines 11 - 98 | |
public function isValid(): bool | |
{ | |
return $this->expiresAt === null || $this->expiresAt > new \DateTimeImmutable(); | |
} | |
} |
Puedes obtener todo esto de los bloques de código de esta página.
A continuación, abre assets/vue/controllers/TreasureConnectApp.vue
... y añade una nueva prop que se pueda pasar: tokens
:
// ... lines 1 - 34 | |
<script setup> | |
// ... lines 36 - 40 | |
const props = defineProps(['entrypoint', 'user', 'tokens']) | |
// ... lines 42 - 47 | |
</script> |
Gracias a eso, tendremos una nueva variable tokens
en la plantilla. Después del enlace "Cerrar sesión", pegaré algo de código que los muestre:
<template> | |
<div class="purple flex flex-col min-h-screen"> | |
// ... lines 3 - 5 | |
<div class="flex-auto flex flex-col sm:flex-row justify-center px-8"> | |
<LoginForm | |
v-on:user-authenticated="onUserAuthenticated"></LoginForm> | |
<div | |
class="book shadow-md rounded sm:ml-3 px-8 pt-8 pb-8 mb-4 sm:w-1/2 md:w-1/3 text-center"> | |
<div v-if="user"> | |
// ... lines 12 - 13 | |
| <a href="/logout" class="underline">Log out</a> | |
<br> | |
<h3 class="text-left font-semibold mt-2">Tokens</h3> | |
<div v-if="null === tokens">Refresh to see tokens...</div> | |
<dl v-else class="text-left max-w-md text-gray-900 divide-y divide-gray-200 dark:divide-gray-700"> | |
<div class="flex flex-col py-3" v-for="token in tokens" :key="token"> | |
<dd class="text-xs whitespace-normal break-words">{{ token }}</dd> | |
</div> | |
</dl> | |
</div> | |
// ... lines 24 - 28 | |
</div> | |
</div> | |
// ... line 31 | |
</div> | |
</template> | |
// ... lines 34 - 49 |
Último paso: abrir templates/main/homepage.html.twig
. Aquí es donde pasaremos props a nuestra aplicación Vue. Pásale uno nuevo llamado tokens
y configúralo como, si app.user
, entoncesapp.user.validTokenStrings
, si no null
:
// ... lines 1 - 2 | |
{% block body %} | |
<div {{ vue_component('TreasureConnectApp', { | |
// ... lines 5 - 6 | |
tokens: app.user ? app.user.validTokenStrings : null | |
}) }}></div> | |
{% endblock %} |
¡Vamos a probarlo! Si actualizamos, ahora mismo no estamos conectados. Utiliza nuestros enlaces tramposos para iniciar sesión. Observa que no los muestra inmediatamente... podríamos mejorar nuestro código para que lo hiciera... pero no es gran cosa. Actualiza y... ¡ahí están! ¡Tenemos dos tokens!
Siguiente paso: vamos a escribir un sistema para que pueda leer estos tokens y autenticar al usuario en lugar de utilizar la autenticación de sesión.
Hello,
When you are using name like ROLE_USER_EDIT, it's to control if the user have the permission to edit or not, so we are more talking about permission than ROLE No ?
I mean ROLE can be for example: ROLE_USER, ROLE_ADMIN, ROLE_SUPER_ADMIN, and a user who have USER ROLE, can have or no the permission to create a post (ex: if is a new user < 30 days: he can't create a post)