Autenticación de usuarios

Uno de los puntos clave de cualquier aplicación web, tanto si está hecha con Angular como si no, es, desde luego, la autenticación de los usuarios. Es evidente que nuestra aplicación tendrá algunas vistas (al menos las vistas de inicio y de acceso de usuarios) a las que podrá acceder cualquier visitante anónimo (entendiendo por tal no autenticado). Sin embargo, también tendrá algunas vistas a las que sólo deberían poder acceder los usuarios registrados, autenticados, y con un permiso tal que se les pueda dar paso a tales contenidos. Por ejemplo, el CRUD de miembros con el que estamos trabajando no debería permitir que cualquiera vea la lista de socios y, mucho menos, que los edite o que cree nuevos socios. Un visitante anónimo no debería tener acceso a estas áreas. Además, deberemos proveer un sistema de login, para que los administradores de la página puedan autenticarse. Y, ya puestos, puede haber dos niveles de administradores: uno que sólo pueda ver la lista de usuarios, y otro que, además, pueda crear nuevos o editar los existentes.

Adicionalmente a todo esto, una vez autenticado un administrador, sus datos deberán permanecer en el navegador durante todo el ciclo de vida de la aplicación, de modo que, al pasar de una vista a otra, se verifique si el usuario que está navegando está autorizado y, en caso contrario, no de le permita acceder. Para esto puede resultarnos muy útil (entre otras cosas) el uso de JWT. Si no estás familiarizado con este sistema de tokens, te sugiero que leas este artículo, y este otro, antes de seguir adelante.

PREPARANDO EL ENTORNO

Antes de modificar nuestra aplicación para poder emplear este sistema, debemos preparar algunas cosas. Por ejemplo, debemos contar con la librería Firebase JWT para poder crear y leer los tokens JWT adecuados para los usuarios autenticados. Dado que estos son procesos de backend, instalaremos la librería dentro del directorio donde tenemos las API’s. Por cuestiones organizativas, crearemos un directorio llamado autenticacion, en el que iremos instalando todo el sistema. Una vez creado, abriremos la terminal de mandatos en dicho directorio y teclearemos:

composer require firebase/php-jwt

Además, en la base de datos, debemos incluir una tabla de administradores, que serán los que podrán acceder a los usuarios. La tabla se llamará admins, y su estructura será la siguiente:

En esta tabla vamos a crear dos administradores, uno con el rol ROLE_ADMIN, que sólo podrá ver la lista de miembros, y otro con el rol ROLE_FULL, que, además de ver la lista, tendrá permiso para editar los datos de los miembros y crear nuevos miembros. Los datos de la tabla quedan así:

Las contraseñas, como ves, están hasheadas, que es como lo haríamos en un proyecto real. En pruebas podríamos haberlas guardado en plano pero, en un proyecto para producción eso no lo haríamos nunca. Por tanto, las ponemos emulando el proyecto real. La contraseña del administrador admin en este ejemplo es, simplemente, admin; la contraseña del administrador admin_full es, precisamente, admin_full. Esto también es algo que nunca haríamos en un proyecto real pero, en este ejemplo, vamos a ser un poco tolerantes y nos vamos a perdonar eso.

CAMBIOS EN LA APLICACIÓN

Aquí viene la parte que nos interesa. Ya sabemos qué es y como usar JWT para autenticar a un usuario de una aplicación y ya hemos creado nuestra tabla de administradores (que serán los que puedan autenticarse, para gestionar a los usuarios). Ahora tenemos que hacer que esto funcione en la aplicación.

Tenemos que crear un servicio que sea el que se ocupe de determinar si existe un token y que, en caso de que así sea, verificar que el usuario es quien dice ser (es decir, que el token no haya sido manipulado), y que el token no ha expirado. Si existe un token, y este es correcto, se le mostrarán al administrador las opciones a las que tenga acceso según su nivel (ya hemos comentado que consideraremos dos niveles de administrador, pero podríamos tener más). Si no existe un token, las únicas vistas a las que podrá tener acceso el cisitante son la página de inicio y aquella que le permite loguearse como administrador con su nombre de usuario y contraseña.

Este servicio deberá, por tanto, estar disponible para toda la aplicación. Así pues, lo crearemos en la ruta raíz de la misma (src/app/services), y lo proveeremos en el modulo raíz (AppModule). Después, podremos inyectarlo en todos los componentes, de modo que, cada vez que tratemos de acceder a uno de estos, se verifique el token.

Necesitaremos crear lo siguiente:

  • El servicio que determina si existe un token, y verifica que este sea correcto y no haya expirado. Lo haremos así:

ng g service services/CheckLogin

  • El componente que permite a un visitante loguearse como administrador, y el componente que permite a un administrador cerrar la sesión actual. Aunque vamos a emplear el sessionStorage que, como sabemos, «muere» al cerrar el navegador, debemos darle al administrador la opción de cerrar la sessión manualmente, lo que transmite una mayr sensación de seguridad. Estos los crearemos dentro de un módulo específico. Lo haremos así:

ng g m admins
ng g c admins/AdminLogin
ng g c admins/AdminLogout

  • Un componente especial que muestre un mensaje de «Acceso prohibido», si alguien que no se ha autenticado como administrador intenta acceder a una vista para la que no tienen permiso. El concepto es similar al de la vista de «Not found»: darle a la persona un mensaje claro de cual es el problema. Lo haremos así:

ng g c globals/forbidden

Además, necesitaremos hacer otros cambios en la aplicación:

  • Inyectar el servicio en aquellos componententes que, para mostrar una vista, deban comprobar si el usuario está autenticado.
  • Proveer, como hemos dicho, el servicio en AppModule, lo que lo deja disponible para todo el sitio.
  • Modificar el enrutador principal, para proveer el acceso a los componentes de Login y Logout, así como, en su caso, al componente de acceso prohibido.
  • Modificar la barra de navegación para que, en caso de que el usuario sea un visitante, no le muestre las opciones que están reservadas a los administradores. Llegados a este punto, te preguntarás que si a un visitante no se le muestran las opciones reservadas a los administradores ¿por qué necesitamos inyectar el sevicio de comprobación en los componentes de listar o editar los usuarios? Bien. Aunque las opciones no se muestren en la barra de navegación, el visitante podría querer acceder tecleando una URL en la barra de direcciones. Por esta razón, siempre que entre en un componente al que no está autorizado se debe comprobar esto, y mandarle a la página de acceso prohibido.

Además, deberemos crear las correspondientes API’s, que permitan a un administrador loguearse, y que compueben, si hay un token, si este es correcto, y corresponde a un administrador logueado.

EL SERVICIO CheckAdminService

Este servicio es el que se encargará de determinar si existe el token. En caso de no existir, pondrá valores predeterminados en datos como el nombre del administrador o su rol (tipo cadenas vacías, o undefined, o lo que decidamos). En caso de existir, en cada llamada verificará el token y, si este aún no ha expirado, almacenará en sessionStorage los datos que necesitemos. Por lo tanto, ya sabemos que este servicio debe «vivir» en la raíz de la aplicación y actuará sobre la sesión en curso.

El servicio en sí no tiene nada que no conozcamos ya. Como podrás ver suando examiens el código, se encarga de verificar si existe un token o no. Si no existe, pone una cadena vacía en las variables de sessionStorage nombreAdmin y rolAdmin. Si existe, llama a la API que lo comprueba. Si es correcto, pone los valores adecuados en las variables mencionadas. Como podrás ver, el código no puede ser más simple.

Este servicio debe estar proveido, como hemos comentado, en AppModule, concretamente en app.module.ts, como ya hemos comentado. Lo haremos así:

Como ves, en esto no hay nada nuevo. Es como cualquier otro servicio. Debemos proveerlo allí donde lo vayamos a necesitar (en este caso, al ser

INYECTANDO EL SERVICIO

En cada componente que sea susceptible de ser usado por un administrador, pero no por un visitante, (o deba ser accesible pero mostrando diferentes contenidos) debemos inyectar el servicio. Como ejemplo, vamos a ver como hacerlo en HomeComponent, ya que, en los demás componentes el mecanismo es el mismo:

En las líneas resaltadas vemos como se importa el servicio, como se implementa en el constructor de la clase y, por último, lo más importante, como se llama al método del servicio que se encarga de verificar el token, de modo que las variables de sessionStorage se mantengan actualizadas.

En los demás componentes es lo mismo. Lo único que cambia es que, una vez actualizadas las variables, debemos comprobarlas para mostrar u ocultar determinadas opciones. Por ejemplo, en el componente MembersListComponent hacemos varias cosas:

Si el usuario es un visitante, le redireccionamos a la página de acceso prohibido.

Si es un administrador de nivel «bajo», le mostraremos la lista de usuarios, pero no los botones de edición, borrado o crear nuevo usuario.

Si es un administrador de nivel alto, si tendrá estas opciones.

REDIRECCIONANDO LA VISTA

Y, ya que hablamos de ello, vamos a ver como redireccionar una vista a otra (pasar de un componente a otro, para entendernos) con Angular. Para ello necesitamos tener un objeto de la clase Router, y usar su método navigateByUrl(). Este no le habíamos visto hasta ahora. Como argumento, le ponemos el nombre del parámetro path del componente al que queremos saltar, por lo que este deberá estar declarado en el enrutador. Vamos por partes. En primer lugar, observa los fragmentos de members-list.component.ts que te muestro a continuación:

Empezamos importando la clase Router y el CheckAdminService. Después, los incluimos en el constructor de nuestra clase, como ya sabemos hacer. Por último, ejecutamos el método checkToken() del servicio y, si no podemos identificar a un administrador, nos redirigimos a la ruta forbidden. Veamos cómo hemos modificado el enrutador:

Como ves, hemos creado los path para el login y el logout, así como uno, con el nombre forbidden, para la página de acceso prohibido. Gracias a este nombre podemos redirecciónar a la vista adecuada con el método que hemos aprendido aquí.

LA BARRA DE NAVEGACIÓN

La barra de navegación es un caso aparte, que requiere más atención. Esto se debe a que es un elemento que tiene que actualizarse, según el visitante esté autenticado como administrador o no. Sin embargo, dado que este componente no se recarga cuando cambiamos de vista, no nos sirve de nada inyectar el servicio y ejecutarlo al inicio, ya que sólo se ejecuta en la primera carga de la aplicación.

La barra de navegación, al igual que la cabecera, son invocados por AppComponent, es decir, el componente raíz. Tenemos que tomar los datos de nombre del administrador y su rol de sessionStorage e inyectarlos en la barra de navegación, de modo que, cuando se llame a cualquier otro componente, se reciban estos datos actualizados. Observa la lógica de NavbarComponent (navbar.component.ts):

En las líneas resaltadas ves como recibimos inyectadas estas variables, que el servicio CheckAdminService proporciona cada vez que se se carga un componente. Sin embargo, para que se inyecten las variables, tenemos que modificar app.component.html, así:

De este modo, siempre tendremos estas variables actualizadas en la barra de navegación. Así, en la vista, podremos usarlas, mediante la directiva estructural *ngIf para determinar que opciones del menú se mostrarán, y cuáles no.

CONCLUYENDO

En este artículo hemos aprendido mucho pero, realmente, pocos conceptos nuevos. Lo que sí hemos aprendido es a usar conceptos que ya conocemos para dotar a nuestra aplicación de nuevas funcionalidades muy llamativas y, desde luego, muy importantes. Como siempre, en este enlace tienes toda la aplicación, en su estado actual, lista para descargar y analizar el código.

   

Deja un comentario