If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeCada ApiToken
tiene una matriz de ámbitos, aunque todavía no la estamos utilizando. La idea es genial: cuando se crea un token, puedes seleccionar qué permisos tiene. Por ejemplo, puede que un token tenga permiso para crear nuevos tesoros, pero no para editar tesoros existentes. Para permitirlo, vamos a asignar los ámbitos de un token a roles en Symfony.
Ahora mismo en ApiTokenHandler
, básicamente devolvemos el usuario... y luego el sistema se autentica completamente como ese usuario. Esto significa que obtenemos los roles que haya en ese objeto User
. ¿Cómo podríamos cambiar eso para que nos autentiquemos como ese usuario... pero con un conjunto diferente de roles? ¿Un conjunto basado en los ámbitos del token?
Estamos utilizando el sistema de seguridad access_token
. Pulsa Shift
+Shift
y abre una clase principal llamada AccessTokenAuthenticator
. Esto es genial: ¡es el código real que hay detrás de ese sistema de autenticación! Por ejemplo, aquí es donde coge el token de la petición y llama al método getUserBadgeFrom()
de nuestro controlador de token.
Los roles que tendrá el usuario también se determinan aquí: abajo dentro decreateToken()
. El "token" es, en cierto modo, una "envoltura" del objeto User
en el sistema de seguridad. Y aquí es donde le pasamos los roles que debe tener. Como puedes ver, pase lo que pase, los roles serán $passport->getUser()->getRoles()
. En otras palabras, siempre obtenemos los roles llamando a getRoles()
en la clase User
... que sólo devuelve la propiedad roles
.
Así que no hay un gran punto de enganche. Podríamos crear una clase autenticadora personalizada e implementar nuestro propio método createToken()
. Pero eso es un fastidio porque tendríamos que reimplementar completamente la lógica de esta clase autenticadora. Así que, en lugar de eso, podemos... hacer una especie de trampa.
Empieza en User
. Desplázate hasta la parte superior, donde están nuestras propiedades. Añade una nueva:private ?array
llamada $accessTokenScopes
e inicialízala a null
:
... lines 1 - 38 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 41 - 54 | |
/* Scopes given during API authentication */ | |
private ?array $accessTokenScopes = null; | |
... lines 57 - 248 | |
} |
Observa que no es una columna persistente. Es sólo un lugar para almacenar temporalmente los ámbitos que debe tener el usuario. A continuación, en la parte inferior añade un nuevo método público llamado markAsTokenAuthenticated()
con un argumento array $scopes
. Vamos a llamarlo durante la autenticación. Dentro, di$this->accessTokenScopes = $scopes
:
... lines 1 - 38 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 41 - 244 | |
public function markAsTokenAuthenticated(array $scopes) | |
{ | |
$this->accessTokenScopes = $scopes; | |
} | |
} |
Aquí es donde las cosas se ponen interesantes. Busca el método getRoles()
. Sabemos que, pase lo que pase, Symfony llamará a esto durante la autenticación y lo que esto devuelva, esos serán los roles que tendrá el usuario. Vamos a "colar" nuestros roles de alcance.
En primer lugar, si la propiedad $accessTokenScopes
es null
, significa que estamos iniciando sesión como un usuario normal. En este caso, establece $roles
en $this->roles
para que obtengamos todos los $roles
en User
. A continuación, añade un rol extra llamado ROLE_FULL_USER
:
... lines 1 - 38 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 41 - 113 | |
public function getRoles(): array | |
{ | |
if (null === $this->accessTokenScopes) { | |
// logged in via the full user mechanism | |
$roles = $this->roles; | |
$roles[] = 'ROLE_FULL_USER'; | |
} else { | |
... line 121 | |
} | |
... lines 123 - 127 | |
} | |
... lines 129 - 248 | |
} |
Hablaremos de ello en un minuto.
Por otra parte, si iniciamos sesión mediante un token de acceso, digamos $roles = $this->accessTokenScopes
:
... lines 1 - 38 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 41 - 113 | |
public function getRoles(): array | |
{ | |
if (null === $this->accessTokenScopes) { | |
// logged in via the full user mechanism | |
$roles = $this->roles; | |
$roles[] = 'ROLE_FULL_USER'; | |
} else { | |
$roles = $this->accessTokenScopes; | |
} | |
... lines 123 - 127 | |
} | |
... lines 129 - 248 | |
} |
Y, en ambos casos, asegúrate de que siempre tenemos ROLE_USER
:
... lines 1 - 38 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 41 - 113 | |
public function getRoles(): array | |
{ | |
if (null === $this->accessTokenScopes) { | |
// logged in via the full user mechanism | |
$roles = $this->roles; | |
$roles[] = 'ROLE_FULL_USER'; | |
} else { | |
$roles = $this->accessTokenScopes; | |
} | |
// guarantee every user at least has ROLE_USER | |
$roles[] = 'ROLE_USER'; | |
return array_unique($roles); | |
} | |
... lines 129 - 248 | |
} |
Una vez hecho esto, dirígete a ApiTokenHandler
. Justo antes de devolverUserBadge
, añade $token->getOwnedBy()->markAsTokenAuthenticated()
y pasa$token->getScopes()
:
... lines 1 - 10 | |
class ApiTokenHandler implements AccessTokenHandlerInterface | |
{ | |
... lines 13 - 16 | |
public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge | |
{ | |
... lines 19 - 28 | |
$token->getOwnedBy()->markAsTokenAuthenticated($token->getScopes()); | |
return new UserBadge($token->getOwnedBy()->getUserIdentifier()); | |
} | |
} |
¡Listo! ¡Vamos a probarlo! De vuelta a Swagger, ya tiene nuestro token de API... así que podemos volver a ejecutar la petición. Genial: vemos la cabecera Authorization
. ¿Se ha autenticado con los ámbitos correctos?
Haz clic para abrir el perfil de esa petición... y dirígete a "Seguridad". ¡Lo hizo! Mira: hemos iniciado sesión como ese usuario, pero con ROLE_USER
, ROLE_USER_EDIT
y ROLE_TREASURE_CREATE
: los dos ámbitos del token. Pero si iniciáramos sesión a través del formulario de acceso, en lugar de estos ámbitos, tendríamos los roles que el usuario tenga normalmente, además de ROLE_FULL_USER
.
En el próximo capítulo, utilizaremos estos roles para proteger distintas operaciones de la API. Por ejemplo, para utilizar la ruta POST tesoros, necesitaremos ROLE_TREASURE_CREATE
. Pero también tenemos que asegurarnos de que si un usuario se conecta a través del formulario de acceso, pueda utilizar esta operación, aunque no tenga exactamente ese rol. Ahí es donde ROLE_FULL_USER
resulta útil.
Abre config/packages/security.yaml
y, en cualquier lugar, añade role_hierarchy
... Te recomiendo que lo escribas correctamente. Di ROLE_FULL_USER
. Así, si has iniciado sesión como usuario completo, vamos a darte todos los ámbitos posibles que podría tener un token. Copia los tres ámbitos de los roles: ROLE_USER_EDIT
, ROLE_TREASURE_CREATE
y ROLE_TREASURE_EDIT
:
security: | |
... lines 2 - 12 | |
role_hierarchy: | |
ROLE_FULL_USER: [ROLE_USER_EDIT, ROLE_TREASURE_CREATE, ROLE_TREASURE_EDIT] | |
... lines 15 - 56 |
Debemos asegurarnos de que si añadimos más ámbitos, también los añadimos aquí.
Gracias a esto, si protegemos algo requiriendo ROLE_USER_EDIT
, los usuarios que se registren a través del formulario de acceso tendrán acceso.
Bien equipo, ¡hemos terminado con la autenticación! ¡Guau! A continuación, vamos a empezar con la "autorización", aprendiendo a bloquear operaciones para que sólo puedan acceder a ellas determinados usuarios.
"Houston: no signs of life"
Start the conversation!
// 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
}
}