3.4.04. Autenticación segura - diezMalena/api_FCTFiller GitHub Wiki

Servidor

Situación inicial y objetivos

Al inicio del desarrollo de la funcionalidad de autenticación segura, el proyecto contaba con una autenticación no segura (sin token) que comprobaba los datos de inicio de sesión en una view de SQL, que aglutinaba los datos de todos los usuarios del sistema. Para añadir una capa de seguridad a nuestro sitio, es necesario que se implemente la autenticación mediante token.

Para ello, se han utilizado las herramientas que ofrece Laravel, con objeto de facilitar el trabajo al equipo de desarrollo y ofrecer unos servicios más seguros a los usuarios.

Integración de Passport

Passport es un paquete que permite gestionar la autenticación segura en Laravel de forma sencilla. Se eligió esta tecnología en lugar de Sanctum ya que Passport es más segura y nos permite gestionar los token bearer, lo cual nos facilita las cosas del lado del cliente con Angular.

El proceso que se siguió para incluir Passport en nuestra API está contemplado en este tutorial, complementado con la documentación oficial de Laravel 8. En resumen, se hizo lo siguiente:

  1. Incorporación del paquete al proyecto. Se ejecuta el siguiente comando en la carpeta del proyecto:

    composer require laravel/passport

  2. En el archivo del proyecto app/Providers/AppServiceProvider.php introducimos dos líneas:

    use Illuminate\Support\Facades\Schema; // En la parte superior del archivo, con las importaciones
    
    Schema::defaultstringLength(191); // Dentro de la función boot()
    
  3. Ejecutamos las migrations:

    php artisan migrate

    En caso de que haya error, es posible que el proyecto tenga almacenados archivos de migrations que se han eliminado y son inconsistentes. Para solucionarlo, habría que ejecutar el comando composer dump-autoload

  4. Creamos las claves de encriptado:

    php artisan passport:install

  5. Incluimos las funcionalidades de manejo de tokens al modelo User, incluyendo estas dos líneas:

    use Laravel\Passport\HasApiTokens; // En la parte superior del archivo, con las importaciones. Eliminamos la línea de Sanctum
    
    use HasApiTokens; // Lo añadimos junto a los demás "use"
    
  6. Llamamos a las rutas de Passport en app/Providers/AuthServiceProvider.php. Descomentamos el interior del vector $providers y añadimoso las dos siguientes líneas al archivo:

    use Laravel\Passport\Passport; // En la parte superior del archivo, con las importaciones
    
    Passport::routes(); // Dentro del método boot(), debajo de lo que haya escrito
    
  7. Configuramos el controlador en config/auth.php, añadiendo al valor de 'guards' la parte de 'api' del siguiente código:

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
            'hash' => false,
        ],
    ],
    

Migración de view a users

Para poder implementar el token de autenticación de forma sencilla, Laravel ofrece el Model User, que cuenta con los atributos, características y métodos básicos de un usuario estándar de aplicación web. Entre otras cosas, permite dar la opción de recordar el token, guardándolo en la base de datos; y oculta este atributo y la contraseña por defecto.

Dado que la estructura básica de User no era suficiente para la información que necesitábamos almacenar, se añadió el atributo tipo al modelo y a la migration correspondiente, que indica si el usuario es alumno, profesor o trabajador de una empresa. Sin embargo, la tabla de usuarios ya estaba migrada, así que se hubo de crear una migration específica:

php artisan make:migration add_new_fields_to_users_table

Dentro del archivo generado, se añadió el campo al Schema de la tabla:

Schema::table('users', function (Blueprint $table) {
    $table->string('tipo')->after('remember_token');
});

El siguiente paso era importar todos los datos de la view de usuarios a la tabla users. Para ello, se introdujo tras la definición de la tabla un insert masivo (con Query Builder), basándonos en la SELECT que generaba la vista.

Para que estos cambios fueran masivos, se ejecutó esta nueva migration mediante el comando:

php artisan migrate:refresh --path=database/migrations/2022_04_25_101906_add_new_fields_to_users_table.php

Login

Ahora podemos hacer el método de login en el servidor. La integración del modelo User nos permite hacer la comprobación del usuario de forma muy sencilla:

if (!auth()->attempt($loginData)) {
    return response()->json(['message' => 'Login incorrecto'], 400);

Asimismo, podemos extraer el modelo del usuario y generar el token con dos líneas de código. Después, el usuario que enviamos en la respuesta junto con el token se extrae mediante un método auxiliar ya presente en la aplicación, que construye un objeto con los datos del usuario que necesita el cliente.

$user = auth()->user();
$token = $user->createToken('authToken')->accessToken;
$usuario = Auxiliar::getDatosUsuario($user);

Protección de rutas - Middlewares

Para proteger las rutas de la API, se utilizan los middlewares de Laravel. Para usarlos, se debe crear el archivo con el comando php artisan make:middleware NombreMiddleware e incluir su ruta en el archivo Kernel.php. Se utilizan tres clases de middleware de autenticación:

  • Autenticación automática. Comprueba si el token corresponde a un usuario autenticado. Se incorpora a una ruta como 'auth:api', justo después de 'Cors'.
  • Autenticación de perfil. Comprueba si el token corresponde a un usuario con cierto perfil. No debe ponerse más de uno en cada ruta o grupo de rutas. Se incorpora después del middleware 'auth:api':
    • 'alumno': deja pasar a los usuarios con perfil de alumno.
    • 'profesor': deja pasar a los usuarios con perfil de profesor.
    • 'trabajador': deja pasar a los usuarios con perfil de trabajador.
  • Autenticación de rol. Comprueba si el token corresponde a un usuario con cierto rol. Hace la comprobación de perfil previa. No debe ponerse más de uno en cada ruta. Se incorpora después de 'auth:api':
    • 'director': deja pasar a los usuarios con perfil de profesor y rol de director.
    • 'jefatura': deja pasar a los usuarios con perfil de profesor y rol de jefatura o director.
    • 'tutor': deja pasar a los usuarios con perfil de profesor y rol de tutor.
    • 'alumno_profesor': deja pasar a los usuarios con perfi de profesor y de alumno.
    • 'representante': deja pasar a los usuarios con perfil de trabajador y rol de representante legal.
    • 'responsable': deja pasar a los usuarios con perfil de trabajador y rol de responsable de centro de trabajo.
    • 'tutor_empresa': deja pasar a los usuarios con perfil de trabajador y rol de tutor de empresa o de responsable de centro de trabajo.

Un ejemplo de aplicación de middlewares en api.php sería:

Route::group(['middleware' => ['Cors', 'auth:api', 'profesor']], function () {
    // Si se quiere añadir un middleware extra a una sola ruta:
    Route::any('prueba', [ControladorGenerico::class, 'prueba'])->middleware('tutor');
});

Funciones CRUD de usuarios

Dado que el registro de usuarios se había hecho en otras funcionalidades antes de implementar ésta, se crearon cuatro métodos básicos de CRUD para la nueva tabla users:

  • Create (añadir): addUser(Model $user, string $perfil) - Devuelve un código estándar http.
  • Read (obtener): getUser(string $email) - Devuelve un objeto tipo User o un código estándar http.
  • Update (modificar): updateUser(Model $user, string $email) - Devuelve un código estándar http.
  • Delete (eliminar): deleteUser(string $email) - Devuelve un código estándar http.

Estos métodos se incluyen en la clase Auxiliar, dado que están planteados como de uso interno. Para facilitar la migración de esta funcionalidad, addUser y updateUser reciben un Model completo de tipo Alumno, Profesor o Trabajador, de forma que la implementación se pueda hacer en una sola línea de código. Esto es posible gracias a que los atributos necesarios para rellenar esta tabla tienen los mismos nombres en las tres tablas correspondientes.

Cliente

Guardado del usuario y el token en sesión

Para guardar, obtener y eliminar el usuario y el token de sesión se usan los métodos del servicio auxiliar LoginStorageService:

  • Usuario: setUser(user: Usuario), getUser(), removeUser() para guardar, obtener y eliminar el usuario de la sesión, respectivamente.
  • Token: setTokenSession(access_token: string), getTokenFromSession(), removeToken() para guardar, obtener y eliminar el token de la sesión, respectivamente.
  • Ambos: setUserWithToken(user: Usuario, access_token: string), removeUserAndToken() para guardar y eliminar tanto el usuario como el token de la sesión, respectivamente.

Gestión de cabeceras (HttpHeaders)

Dado que la mayoría de peticiones al servidor necesitarán unas cabeceras con el token incrustado, se ha creado el servicio auxiliar HttpHeadersService para crear cabeceras de distintos tipos:

  • getHeadersWithoutToken() devuelve un objeto headers sin token de accesso.
  • getHeadersWithToken() devuelve las headers con el token de acceso.
  • getHeadersWithTokenArrayBuffer() devuelve un objeto HTTPOptions con el token en las headers. Al sustituirse por las headers en la petición al servidor, permite la descarga de un archivo.

Protección de rutas - Guards

Proteger las rutas en el cliente implica que sólo puedan acceder a ciertas funcionalidades los usuarios que cumplan ciertas condiciones, como haber iniciado sesión o poseer ciertos privilegios. Esto se hizo con los Guards de Angular, que permiten o impiden el paso según las condiciones que se determinen. En este caso, se han creado Guards de sólo dos tipos: CanActivate, que regula el componente en sí; y CanActivateChild, que regula el componente y sus hijos.

Se han creado tres Guards:

  • LoggedGuard, de tipo CanActivateChild. Permite el paso si se intenta acceder a la ruta una vez se ha hecho login. Se aplica a los módulos que requieran un login, como DataManagement y DataUpload. Dado que se aplica a los módulos, no es necesario aplicarlo a las rutas de componentes.

  • PerfilesGuard, de tipo CanActivate. Debe recibir un vector de strings con los perfiles a los que se permite pasar al componente. Este vector se envía dentro del objeto data con la ruta correspondiente (ver ejemplo más abajo).

  • Guards de perfiles específicos, de tipo CanActivate. Sólo se debe pasar uno de estos guards simultáneamente. Hasta el momento, se ha hecho uno por cada perfil:

    • AlumnosGuard: deja pasar a los alumnos.
    • ProfesoresGuard: deja pasar a los profesores. Se puede enviar como data un atributo roles que contenga un vector de números (correspondientes a los roles del sistema), de forma que restrinja el acceso a los profesores con ciertos roles. Si no se se envían los roles, deja pasar a todos los profesores.
    • TrabajadoresGuard: deja pasar a los trabajadores. Su funcionamiento respecto a los roles es el igual que ProfesoresGuard.

    En caso de que se necesite restringir el acceso de forma más específica (una ruta a la que sólo deban acceder tutores de empresa, tutores de centro y alumnos, por ejemplo), se deberá crear un guard y documentar.

Los Guards se integran en los archivos de rutas de cada módulo (*-routing.module.ts) de la siguiente manera:

const routes: Routes = [
    // Ejemplo de logged (módulo)
    {
        path: 'data-management',
        loadChildren: () =>
            import('./modules/data-management/data-management.module').then(
                (m) => m.DataManagementModule
            ),
        canActivateChild: [LoggedGuard],
    },
    // Ejemplo de perfiles y roles (componentes)
    {
        path: 'gestion-alumnos',
        component: GestionAlumnosComponent,
        canActivate: [ProfesoresGuard],
        data: {
            roles: [1, 2],
        },
    },
    {
        path: 'ruta-trabajadores',
        component: GestionAlumnosComponent,
        canActivate: [TrabajadoresGuard],      
    },
];