Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Manejo de errores de autenticació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.

Cuando iniciamos sesión con un correo electrónico y una contraseña no válidos, parece que el sistema json_logindevuelve un bonito JSON con una clave error establecida en "Credenciales no válidas". Si quisiéramos personalizar esto, podríamos crear una clase que implementeAuthenticationFailureHandlerInterface:

class AppAuthFailureHandler implements AuthenticationFailureHandlerInterface
{
    public function onAuthenticationFailure($request, $exception)
    {
        return new JsonResponse(
            ['something' => 'went wrong'],
            401
        );
    }
}

Y luego establecer su ID de servicio en la opción failure_handler en json_login:

json_login:
    failure_handler: App\Security\AppAuthFailureHandler

Mostrar el error en el formulario

Pero, esto nos sirve de sobra. Así que vamos a utilizarlo en nuestro/assets/vue/LoginForm.vue. No profundizaremos demasiado en Vue, pero ya tengo un estado llamado error, y si lo configuramos, se mostrará en el formulario:

... lines 1 - 48
<script setup>
... lines 50 - 54
const error = ref('');
... lines 56 - 65
const handleSubmit = async () => {
... line 67
error.value = '';
... lines 69 - 82
if (!response.ok) {
const data = await response.json();
console.log(data);
// TODO: set error
return;
}
... lines 90 - 93
}
</script>

Después de hacer la petición, si la respuesta no está bien, ya estamos descodificando el JSON. Ahora digamos que error.value = data.error:

... lines 1 - 48
<script setup>
... lines 50 - 65
const handleSubmit = async () => {
... lines 67 - 82
if (!response.ok) {
const data = await response.json();
error.value = data.error;
return;
}
... lines 89 - 92
}
</script>

Para ver si funciona, asegúrate de que tienes Webpack Encore ejecutándose en segundo plano para que recompile nuestro JavaScript. Actualiza. Y... puedes hacer clic en este pequeño enlace para hacer trampas e introducir un correo electrónico válido. Pero luego escribe una contraseña ridícula y... ¡Me encanta! ¡Vemos "Credenciales no válidas" en la parte superior con unos recuadros rojos!

json_login Requires Content-Type: application/json

Así que la llamada AJAX funciona de maravilla. Sin embargo, hay un problema con el mecanismo de seguridad json_login: requiere que envíes una cabecera Content-Type configurada comoapplication/json. Nosotros lo establecemos en nuestra llamada Ajax y tú también deberías hacerlo:

... lines 1 - 48
<script setup>
... lines 50 - 65
const handleSubmit = async () => {
... lines 67 - 69
const response = await fetch('/login', {
... line 71
headers: {
'Content-Type': 'application/json'
},
... lines 75 - 78
});
... lines 80 - 92
}
</script>

Pero... si alguien se olvida, queremos asegurarnos de que las cosas no se vuelven completamente locas.

Comenta esa cabecera Content-Type para que podamos ver qué ocurre:

... lines 1 - 48
<script setup>
... lines 50 - 65
const handleSubmit = async () => {
... lines 67 - 69
const response = await fetch('/login', {
... line 71
headers: {
//'Content-Type': 'application/json'
},
... lines 75 - 78
});
... lines 80 - 92
}
</script>

Luego muévete, actualiza la página... escribe una contraseña ridícula y... ¿se borra el formulario? Mira la llamada a la Red. ¡La ruta devolvió un código de estado 200 con una clave user establecida en null!

Y... ¡eso tiene sentido! Como nos falta la cabecera, el mecanismo json_login no hizo nada. En su lugar, la petición continuó a nuestro SecurityController... excepto que esta vez el usuario no está conectado. Así que devolvemos user: null... con un código de estado 200.

Esto es un problema porque hace que parezca que la llamada Ajax ha tenido éxito. Para solucionarlo, si, por cualquier motivo, se omitió el mecanismo json_login... pero el usuario accede a nuestra ruta de inicio de sesión, devolvamos un código de estado 401 que diga:

¡Oye! ¡Necesitas iniciar sesión!

Entonces, si no es $user, entonces return $this->json()... y esto podría parecerse a cualquier cosa. Incluyamos una clave error que explique lo que probablemente salió mal: esto coincide con la claveerror que json_login devuelve cuando fallan las credenciales, así que a nuestro JavaScript le gustará esto. Caramba. ¡Incluso corregiré mi errata!

... lines 1 - 9
class SecurityController extends AbstractController
{
... line 12
public function login(#[CurrentUser] $user = null): Response
{
if (!$user) {
return $this->json([
'error' => 'Invalid login request: check that the Content-Type header is "application/json".',
], 401);
}
... lines 20 - 23
}
}

Y lo más importante, para el segundo argumento, pasa un 401 para el código de estado.

A continuación, podemos simplificar... porque ahora sabemos que habrá un usuario:

... lines 1 - 9
class SecurityController extends AbstractController
{
... line 12
public function login(#[CurrentUser] $user = null): Response
{
if (!$user) {
return $this->json([
'error' => 'Invalid login request: check that the Content-Type header is "application/json".',
], 401);
}
return $this->json([
'user' => $user->getId(),
]);
}
}

¡Hermoso! Gira y envía otra contraseña incorrecta. ¡Precioso! El código de estado 401 activa nuestro código de gestión de errores, que muestra el error en la parte superior. Maravilloso.

Vuelve a LoginForm.vue y pon de nuevo la cabecera Content-Type:

... lines 1 - 48
<script setup>
... lines 50 - 65
const handleSubmit = async () => {
... lines 67 - 69
const response = await fetch('/login', {
... line 71
headers: {
'Content-Type': 'application/json'
},
... lines 75 - 78
});
... lines 80 - 92
}
</script>

Siguiente: vamos a iniciar sesión con éxito y... ¡a averiguar qué queremos hacer cuando eso ocurra! También vamos a hablar de la sesión y de cómo autentica nuestras peticiones a la API.

Leave a comment!

2
Login or Register to join the conversation
Ek24 Avatar

i think you put the coding-challenge before this chapter by mistake.

btw. Ryan ... my cats missing the cat-lang of the courses. they still stuck on Api-Platform 2.6.

Reply

Hey Ek24,

Agree, I reattached that challenge to the next chapter - it makes more sense to show it there, thanks for reporting!

my cats missing the cat-lang of the courses. they still stuck on Api-Platform 2.6.

Oh, sorry, we're still working on translating those remaining chapters into cat's language... but you know those cats, they are too lazy to fit things with deadlines ;)

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