Tutorial de JSON Web Token: un ejemplo en Laravel y AngularJS

Publicado: 2022-03-11

Con la creciente popularidad de las aplicaciones de una sola página, las aplicaciones móviles y los servicios RESTful API, la forma en que los desarrolladores web escriben el código de back-end ha cambiado significativamente. Con tecnologías como AngularJS y BackboneJS, ya no dedicamos mucho tiempo a crear marcas, sino que creamos API que consumen nuestras aplicaciones front-end. Nuestro back-end tiene más que ver con la lógica comercial y los datos, mientras que la lógica de presentación se traslada exclusivamente al front-end o las aplicaciones móviles. Estos cambios han dado lugar a nuevas formas de implementar la autenticación en las aplicaciones modernas.

La autenticación es una de las partes más importantes de cualquier aplicación web. Durante décadas, las cookies y la autenticación basada en servidor fueron la solución más sencilla. Sin embargo, manejar la autenticación en las aplicaciones móviles y de una sola página modernas puede ser complicado y exige un mejor enfoque. Las soluciones más conocidas a los problemas de autenticación de las API son OAuth 2.0 y JSON Web Token (JWT).

Antes de entrar en este tutorial de JSON Web Token, ¿qué es exactamente un JWT?

¿Qué es un token web JSON?

Se utiliza un token web JSON para enviar información que se puede verificar y confiar mediante una firma digital. Comprende un objeto JSON compacto y seguro para URL, que se firma criptográficamente para verificar su autenticidad y que también se puede cifrar si la carga útil contiene información confidencial.

Debido a su estructura compacta, JWT generalmente se usa en encabezados de Authorization HTTP o parámetros de consulta de URL.

Estructura de un token web JSON

Un JWT se representa como una secuencia de valores codificados en base64url que están separados por caracteres de punto.

Ejemplo de token web JSON en laravel y angularjs

Aquí hay un ejemplo de token JWT:

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0 . yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw

Encabezamiento

El encabezado contiene los metadatos del token y, como mínimo, contiene el tipo de firma y el algoritmo de cifrado. (Puede usar una herramienta de formateo JSON para embellecer el objeto JSON).

Encabezado de ejemplo

 { "alg": "HS256", "typ": "JWT" }

Este encabezado de ejemplo de JWT declara que el objeto codificado es un token web JSON y que está firmado con el algoritmo HMAC SHA-256.

Una vez que esto está codificado en base64, tenemos la primera parte de nuestro JWT.

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Carga útil (reclamos)

En el contexto de JWT, un reclamo se puede definir como una declaración sobre una entidad (generalmente, el usuario), así como metadatos adicionales sobre el token en sí. El reclamo contiene la información que queremos transmitir y que el servidor puede usar para manejar correctamente la autenticación de JSON Web Token. Hay múltiples reclamos que podemos proporcionar; estos incluyen nombres de reclamos registrados, nombres de reclamos públicos y nombres de reclamos privados.

Reclamaciones JWT registradas

Estos son los reclamos que están registrados en el registro de reclamos de token web JSON de IANA. Estas notificaciones JWT no pretenden ser obligatorias, sino proporcionar un punto de partida para un conjunto de notificaciones útiles e interoperables.

Éstos incluyen:

  • iss : El emisor del token
  • sub : El sujeto del token
  • aud : La audiencia del token
  • exp : tiempo de caducidad de JWT definido en tiempo de Unix
  • nbf : tiempo “No antes” que identifica el tiempo antes del cual el JWT no debe ser aceptado para procesamiento
  • iat : "Emitido a las" hora, en tiempo de Unix, en la que se emitió el token
  • jti : el reclamo de ID de JWT proporciona un identificador único para el JWT

Reclamos Públicos

Las reclamaciones públicas deben tener nombres resistentes a colisiones. Al convertir el nombre en un URI o URN, se evitan las colisiones de nombres para los JWT en los que el remitente y el receptor no forman parte de una red cerrada.

Un ejemplo de un nombre de reclamo público podría ser: https://www.toptal.com/jwt_claims/is_admin , y la mejor práctica es colocar un archivo en esa ubicación que describa el reclamo para que se pueda quitar la referencia para la documentación.

Reclamos privados

Los nombres de notificación privados pueden usarse en lugares donde los JWT solo se intercambian en un entorno cerrado entre sistemas conocidos, como dentro de una empresa. Estas son afirmaciones que podemos definir nosotros mismos, como ID de usuario, roles de usuario o cualquier otra información.

El uso de nombres de notificaciones que pueden tener significados semánticos conflictivos fuera de un sistema cerrado o privado está sujeto a colisión, así que utilícelos con precaución.

Es importante tener en cuenta que queremos mantener un token web lo más pequeño posible, así que use solo los datos necesarios dentro de los reclamos públicos y privados.

Ejemplo de carga útil de JWT

 { "iss": "toptal.com", "exp": 1426420800, "https://www.toptal.com/jwt_claims/is_admin": true, "company": "Toptal", "awesome": true }

Esta carga útil de ejemplo tiene dos reclamos registrados, un reclamo público y dos reclamos privados. Una vez que está codificado en base64, tenemos la segunda parte de nuestro JWT.

 eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0

Firma

El estándar JWT sigue la especificación JSON Web Signature (JWS) para generar el token firmado final. Se genera combinando el encabezado JWT codificado y la carga útil JWT codificada, y firmándolo con un algoritmo de cifrado fuerte, como HMAC SHA-256. El servidor conserva la clave secreta de la firma, por lo que podrá verificar los tokens existentes y firmar otros nuevos.

 $encodedContent = base64UrlEncode(header) + "." + base64UrlEncode(payload); $signature = hashHmacSHA256($encodedContent);

Esto nos da la parte final de nuestro JWT.

 yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw

Seguridad y cifrado JWT

Es fundamental utilizar TLS/SSL junto con JWT para evitar ataques de intermediarios. En la mayoría de los casos, esto será suficiente para cifrar la carga útil de JWT si contiene información confidencial. Sin embargo, si queremos agregar una capa adicional de protección, podemos cifrar la carga útil de JWT usando la especificación JSON Web Encryption (JWE).

Por supuesto, si queremos evitar la sobrecarga adicional de usar JWE, otra opción es simplemente mantener la información confidencial en nuestra base de datos y usar nuestro token para llamadas API adicionales al servidor siempre que necesitemos acceder a datos confidenciales.

¿Por qué la necesidad de tokens web?

Antes de que podamos ver todos los beneficios de usar la autenticación JWT, debemos ver la forma en que se ha realizado la autenticación en el pasado.

Autenticación basada en servidor

Autenticación basada en servidor

Debido a que el protocolo HTTP no tiene estado, debe haber un mecanismo para almacenar la información del usuario y una forma de autenticar al usuario en cada solicitud posterior después del inicio de sesión. La mayoría de los sitios web utilizan cookies para almacenar la ID de sesión del usuario.

Cómo funciona

El navegador realiza una solicitud POST al servidor que contiene la identificación y la contraseña del usuario. El servidor responde con una cookie, que se configura en el navegador del usuario e incluye una ID de sesión para identificar al usuario.

En cada solicitud posterior, el servidor necesita encontrar esa sesión y deserializarla, porque los datos del usuario se almacenan en el servidor.

Inconvenientes de la autenticación basada en servidor

  • Difícil de escalar : el servidor necesita crear una sesión para un usuario y conservarla en algún lugar del servidor. Esto se puede hacer en la memoria o en una base de datos. Si tenemos un sistema distribuido, debemos asegurarnos de usar un almacenamiento de sesión separado que no esté acoplado al servidor de aplicaciones.

  • Intercambio de solicitudes de origen cruzado (CORS) : al usar llamadas AJAX para obtener un recurso de otro dominio ("origen cruzado"), podríamos tener problemas con las solicitudes prohibidas porque, de forma predeterminada, las solicitudes HTTP no incluyen cookies en cross-origin. solicitudes de origen.

  • Acoplamiento con el marco web : cuando usamos la autenticación basada en servidor, estamos atados al esquema de autenticación de nuestro marco. Es realmente difícil, o incluso imposible, compartir datos de sesión entre diferentes marcos web escritos en diferentes lenguajes de programación.

Autenticación basada en token

Autenticación basada en token

La autenticación basada en token/JWT no tiene estado, por lo que no es necesario almacenar información del usuario en la sesión. Esto nos da la capacidad de escalar nuestra aplicación sin preocuparnos de dónde ha iniciado sesión el usuario. Podemos usar fácilmente el mismo token para obtener un recurso seguro de un dominio que no sea en el que hemos iniciado sesión.

Cómo funcionan los tokens web JSON

Un navegador o un cliente móvil realiza una solicitud al servidor de autenticación que contiene la información de inicio de sesión del usuario. El servidor de autenticación genera un nuevo token de acceso JWT y lo devuelve al cliente. En cada solicitud a un recurso restringido, el cliente envía el token de acceso en la cadena de consulta o en el encabezado de Authorization . Luego, el servidor valida el token y, si es válido, devuelve el recurso seguro al cliente.

El servidor de autenticación puede firmar el token mediante cualquier método de firma seguro. Por ejemplo, se puede usar un algoritmo de clave simétrica como HMAC SHA-256 si hay un canal seguro para compartir la clave secreta entre todas las partes. Alternativamente, también se puede usar un sistema asimétrico de clave pública, como RSA, eliminando la necesidad de compartir más la clave.

Ventajas de la autenticación basada en token

Sin estado, más fácil de escalar : el token contiene toda la información para identificar al usuario, eliminando la necesidad del estado de la sesión. Si usamos un balanceador de carga, podemos pasar al usuario a cualquier servidor, en lugar de estar vinculado al mismo servidor en el que iniciamos sesión.

Reutilización : podemos tener muchos servidores separados, ejecutándose en múltiples plataformas y dominios, reutilizando el mismo token para autenticar al usuario. Es fácil crear una aplicación que comparta permisos con otra aplicación.

Seguridad de JWT : dado que no usamos cookies, no tenemos que protegernos contra los ataques de falsificación de solicitudes entre sitios (CSRF). Todavía debemos cifrar nuestros tokens usando JWE si tenemos que poner información confidencial en ellos y transmitir nuestros tokens a través de HTTPS para evitar ataques de intermediarios.

Rendimiento : no hay una búsqueda del lado del servidor para encontrar y deserializar la sesión en cada solicitud. Lo único que tenemos que hacer es calcular el HMAC SHA-256 para validar el token y analizar su contenido.

Un ejemplo de token web JSON usando Laravel 5 y AngularJS

En este tutorial de JWT, voy a demostrar cómo implementar la autenticación básica usando tokens web JSON en dos tecnologías web populares: Laravel 5 para el código de backend y AngularJS para el ejemplo de aplicación de página única (SPA) de frontend. (Puede encontrar la demostración completa aquí y el código fuente en este repositorio de GitHub para que pueda seguir el tutorial).

Este ejemplo de token web JSON no utilizará ningún tipo de cifrado para garantizar la confidencialidad de la información transmitida en las reclamaciones. En la práctica, esto suele estar bien, porque TLS/SSL cifra la solicitud. Sin embargo, si el token va a contener información confidencial, como el número de seguro social del usuario, también debe cifrarse con JWE.

Ejemplo de servidor de Laravel

Usaremos Laravel para manejar el registro de usuarios, conservar los datos de los usuarios en una base de datos y proporcionar algunos datos restringidos que necesitan autenticación para que la aplicación Angular los consuma. Crearemos un subdominio de API de ejemplo para simular también el uso compartido de recursos de origen cruzado (CORS).

Instalación y arranque de proyectos

Para usar Laravel, tenemos que instalar el administrador de paquetes Composer en nuestra máquina. Al desarrollar en Laravel, recomiendo usar la "caja" preempaquetada de Laravel Homestead de Vagrant. Nos proporciona un entorno de desarrollo completo independientemente de nuestro sistema operativo.

La forma más fácil de iniciar nuestra aplicación JWT Laravel es usar un instalador de Laravel del paquete Composer.

 composer global require "laravel/installer=~1.1"

Ahora estamos listos para crear un nuevo proyecto de Laravel ejecutando laravel new jwt .

Para cualquier pregunta sobre este proceso, consulte la documentación oficial de Laravel.

Después de haber creado la aplicación básica de Laravel 5, debemos configurar nuestro Homestead.yaml , que configurará las asignaciones de carpetas y la configuración de dominios para nuestro entorno local.

Ejemplo de un archivo Homestead.yaml :

 --- ip: "192.168.10.10" memory: 2048 cpus: 1 authorize: /Users/ttkalec/.ssh/public.psk keys: - /Users/ttkalec/.ssh/private.ppk folders: - map: /coding/jwt to: /home/vagrant/coding/jwt sites: - map: jwt.dev to: /home/vagrant/coding/jwt/public - map: api.jwt.dev to: /home/vagrant/coding/jwt/public variables: - key: APP_ENV value: local

Después de que hayamos iniciado nuestra caja Vagrant con el comando vagrant up y hayamos iniciado sesión usando vagrant ssh , navegamos al directorio del proyecto definido previamente. En el ejemplo anterior, sería /home/vagrant/coding/jwt . Ahora podemos ejecutar el comando de php artisan migrate para crear las tablas de usuario necesarias en nuestra base de datos.

Instalación de dependencias de Composer

Afortunadamente, hay una comunidad de desarrolladores que trabajan en Laravel y mantienen muchos paquetes geniales con los que podemos reutilizar y ampliar nuestra aplicación. En este ejemplo, usaremos tymon/jwt-auth , de Sean Tymon, para manejar tokens en el lado del servidor, y barryvdh/laravel-cors , de Barry vd. Heuvel, para manipulación de CORS.

jwt-autorización

Solicite el tymon/jwt-auth en nuestro composer.json y actualice nuestras dependencias.

 composer require tymon/jwt-auth 0.5.*

Agregue JWTAuthServiceProvider a nuestra matriz de proveedores app/config/app.php .

 'Tymon\JWTAuth\Providers\JWTAuthServiceProvider'

Luego, en el archivo app/config/app.php , debajo de la matriz de aliases , agregamos la fachada JWTAuth .

 'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth'

Finalmente, querremos publicar la configuración del paquete usando el siguiente comando: php craft config:publish tymon/jwt-auth

Los tokens web JSON se cifran mediante una clave secreta. Podemos generar esa clave usando el comando php artisan jwt:generate . Se colocará dentro de nuestro archivo config/jwt.php . En el entorno de producción, sin embargo, nunca queremos tener nuestras contraseñas o claves API dentro de los archivos de configuración. En su lugar, deberíamos colocarlos dentro de las variables de entorno del servidor y hacer referencia a ellos en el archivo de configuración con la función env . Por ejemplo:

 'secret' => env('JWT_SECRET')

Podemos obtener más información sobre este paquete y todos sus ajustes de configuración en Github.

laravel-cors

Solicite el barryvdh/laravel-cors en nuestro composer.json y actualice nuestras dependencias.

 composer require barryvdh/laravel-cors 0.4.x@dev

Agregue CorsServiceProvider a nuestra matriz de proveedores app/config/app.php .

 'Barryvdh\Cors\CorsServiceProvider'

Luego agregue el middleware a nuestra app/Http/Kernel.php .

 'Barryvdh\Cors\Middleware\HandleCors'

Publique la configuración en un archivo local config/cors.php usando el comando php artisan vendor:publish .

Ejemplo de configuración de un archivo cors.php :

 return [ 'defaults' => [ 'supportsCredentials' => false, 'allowedOrigins' => [], 'allowedHeaders' => [], 'allowedMethods' => [], 'exposedHeaders' => [], 'maxAge' => 0, 'hosts' => [], ], 'paths' => [ 'v1/*' => [ 'allowedOrigins' => ['*'], 'allowedHeaders' => ['*'], 'allowedMethods' => ['*'], 'maxAge' => 3600, ], ], ];

Enrutamiento y manejo de solicitudes HTTP

En aras de la brevedad, pondré todo mi código dentro del archivo route.php que es responsable del enrutamiento de Laravel y de la delegación de solicitudes a los controladores. Por lo general, crearíamos controladores dedicados para manejar todas nuestras solicitudes HTTP y mantendríamos nuestro código modular y limpio.

Cargaremos nuestra vista AngularJS SPA usando

 Route::get('/', function () { return view('spa'); });

registro de usuario

Cuando hacemos una solicitud POST a /signup con un nombre de usuario y contraseña, intentaremos crear un nuevo usuario y guardarlo en la base de datos. Una vez que se ha creado el usuario, se crea un JWT y se devuelve a través de una respuesta JSON.

 Route::post('/signup', function () { $credentials = Input::only('email', 'password'); try { $user = User::create($credentials); } catch (Exception $e) { return Response::json(['error' => 'User already exists.'], HttpResponse::HTTP_CONFLICT); } $token = JWTAuth::fromUser($user); return Response::json(compact('token')); });

Iniciar sesión de usuario

Cuando hacemos una solicitud POST para /signin con un nombre de usuario y contraseña, verificamos que el usuario existe y devuelve un JWT a través de la respuesta JSON.

 Route::post('/signin', function () { $credentials = Input::only('email', 'password'); if ( ! $token = JWTAuth::attempt($credentials)) { return Response::json(false, HttpResponse::HTTP_UNAUTHORIZED); } return Response::json(compact('token')); });

Obtener un recurso restringido en el mismo dominio

Una vez que el usuario haya iniciado sesión, podemos recuperar el recurso restringido. Creé una ruta /restricted que simula un recurso que necesita un usuario autenticado. Para hacer esto, el encabezado de Authorization de la solicitud o la cadena de consulta deben proporcionar el JWT para que el backend lo verifique.

 Route::get('/restricted', [ 'before' => 'jwt-auth', function () { $token = JWTAuth::getToken(); $user = JWTAuth::toUser($token); return Response::json([ 'data' => [ 'email' => $user->email, 'registered_at' => $user->created_at->toDateTimeString() ] ]); } ]);

En este ejemplo, estoy usando el middleware jwt-auth provisto en el paquete jwt-auth usando 'before' => 'jwt-auth' . Este middleware se utiliza para filtrar la solicitud y validar el token JWT. Si el token no es válido, no está presente o venció, el middleware generará una excepción que podemos detectar.

En Laravel 5, podemos capturar excepciones usando el archivo app/Exceptions/Handler.php . Usando la función de render , podemos crear respuestas HTTP basadas en la excepción lanzada.

 public function render($request, Exception $e) { if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException) { return response(['Token is invalid'], 401); } if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException) { return response(['Token has expired'], 401); } return parent::render($request, $e); }

Si el usuario está autenticado y el token es válido, podemos devolver de forma segura los datos restringidos a la interfaz a través de JSON.

Obtención de recursos restringidos del subdominio API

En el siguiente ejemplo de token web JSON, adoptaremos un enfoque diferente para la validación del token. En lugar de usar el middleware jwt-auth , manejaremos las excepciones manualmente. Cuando hacemos una solicitud POST a un servidor API api.jwt.dev/v1/restricted , estamos haciendo una solicitud de origen cruzado y tenemos que habilitar CORS en el backend. Afortunadamente, ya hemos configurado CORS en el archivo config/cors.php .

 Route::group(['domain' => 'api.jwt.dev', 'prefix' => 'v1'], function () { Route::get('/restricted', function () { try { JWTAuth::parseToken()->toUser(); } catch (Exception $e) { return Response::json(['error' => $e->getMessage()], HttpResponse::HTTP_UNAUTHORIZED); } return ['data' => 'This has come from a dedicated API subdomain with restricted access.']; }); });

Ejemplo de interfaz de AngularJS

Estamos utilizando AngularJS como front-end, confiando en las llamadas API al servidor de autenticación back-end de Laravel para la autenticación de usuarios y datos de muestra, además del servidor API para datos de ejemplo de origen cruzado. Una vez que vayamos a la página de inicio de nuestro proyecto, el backend servirá la resources/views/spa.blade.php que iniciará la aplicación Angular.

Aquí está la estructura de carpetas de la aplicación Angular:

 public/ |-- css/ `-- bootstrap.superhero.min.css |-- lib/ |-- loading-bar.css |-- loading-bar.js `-- ngStorage.js |-- partials/ |-- home.html |-- restricted.html |-- signin.html `-- signup.html `-- scripts/ |-- app.js |-- controllers.js `-- services.js

Bootstrapping de la aplicación angular

spa.blade.php contiene los elementos básicos necesarios para ejecutar la aplicación. Usaremos Twitter Bootstrap para diseñar, junto con un tema personalizado de Bootswatch. Para obtener información visual al realizar una llamada AJAX, usaremos el script de la barra de carga angular, que intercepta las solicitudes XHR y crea una barra de carga. En la sección de encabezado, tenemos las siguientes hojas de estilo:

 <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.superhero.min.css"> <link rel="stylesheet" href="/lib/loading-bar.css">

El pie de página de nuestro marcado contiene referencias a bibliotecas, así como a nuestros scripts personalizados para módulos, controladores y servicios de Angular.

 <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-route.min.js"></script> <script src="/lib/ngStorage.js"></script> <script src="/lib/loading-bar.js"></script> <script src="/scripts/app.js"></script> <script src="/scripts/controllers.js"></script> <script src="/scripts/services.js"></script> </body>

Estamos utilizando la biblioteca ngStorage para AngularJS, para guardar tokens en el almacenamiento local del navegador, de modo que podamos enviarlos en cada solicitud a través del encabezado de Authorization .

En el entorno de producción, por supuesto, minimizaríamos y combinaríamos todos nuestros archivos de script y hojas de estilo para mejorar el rendimiento.

Creé una barra de navegación usando Bootstrap que cambiará la visibilidad de los enlaces apropiados, según el estado de inicio de sesión del usuario. El estado de inicio de sesión está determinado por la presencia de una variable de token en el ámbito del controlador.

 <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">JWT Angular example</a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav navbar-right"> <li data-ng-show="token"><a ng-href="#/restricted">Restricted area</a></li> <li data-ng-hide="token"><a ng-href="#/signin">Sign in</a></li> <li data-ng-hide="token"><a ng-href="#/signup">Sign up</a></li> <li data-ng-show="token"><a ng-click="logout()">Logout</a></li> </ul> </div>

Enrutamiento

Tenemos un archivo llamado app.js que es responsable de configurar todas nuestras rutas de front-end.

 angular.module('app', [ 'ngStorage', 'ngRoute', 'angular-loading-bar' ]) .constant('urls', { BASE: 'http://jwt.dev:8000', BASE_API: 'http://api.jwt.dev:8000/v1' }) .config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) { $routeProvider. when('/', { templateUrl: 'partials/home.html', controller: 'HomeController' }). when('/signin', { templateUrl: 'partials/signin.html', controller: 'HomeController' }). when('/signup', { templateUrl: 'partials/signup.html', controller: 'HomeController' }). when('/restricted', { templateUrl: 'partials/restricted.html', controller: 'RestrictedController' }). otherwise({ redirectTo: '/' });

Aquí podemos ver que hemos definido cuatro rutas que son manejadas por HomeController o RestrictedController . Cada ruta corresponde a una vista HTML parcial. También hemos definido dos constantes que contienen URL para nuestras solicitudes HTTP al backend.

Interceptor de solicitudes

El servicio $http de AngularJS nos permite comunicarnos con el backend y realizar solicitudes HTTP. En nuestro caso, queremos interceptar cada solicitud HTTP e inyectarla con un encabezado de Authorization que contenga nuestro JWT si el usuario está autenticado. También podemos usar un interceptor para crear un controlador de errores HTTP global. Aquí hay un ejemplo de nuestro interceptor que inyecta un token si está disponible en el almacenamiento local del navegador.

 $httpProvider.interceptors.push(['$q', '$location', '$localStorage', function ($q, $location, $localStorage) { return { 'request': function (config) { config.headers = config.headers || {}; if ($localStorage.token) { config.headers.Authorization = 'Bearer ' + $localStorage.token; } return config; }, 'responseError': function (response) { if (response.status === 401 || response.status === 403) { $location.path('/signin'); } return $q.reject(response); } }; }]);

Controladores

En el archivo controllers.js , hemos definido dos controladores para nuestra aplicación: HomeController y RestrictedController . HomeController maneja la funcionalidad de inicio de sesión, registro y cierre de sesión. Pasa los datos de nombre de usuario y contraseña de los formularios de inicio de sesión y registro al servicio Auth , que envía solicitudes HTTP al backend. Luego guarda el token en el almacenamiento local o muestra un mensaje de error, según la respuesta del backend.

 angular.module('app') .controller('HomeController', ['$rootScope', '$scope', '$location', '$localStorage', 'Auth', function ($rootScope, $scope, $location, $localStorage, Auth) { function successAuth(res) { $localStorage.token = res.token; window.location = "/"; } $scope.signin = function () { var formData = { email: $scope.email, password: $scope.password }; Auth.signin(formData, successAuth, function () { $rootScope.error = 'Invalid credentials.'; }) }; $scope.signup = function () { var formData = { email: $scope.email, password: $scope.password }; Auth.signup(formData, successAuth, function () { $rootScope.error = 'Failed to signup'; }) }; $scope.logout = function () { Auth.logout(function () { window.location = "/" }); }; $scope.token = $localStorage.token; $scope.tokenClaims = Auth.getTokenClaims(); }])

RestrictedController se comporta de la misma manera, solo que obtiene los datos mediante las funciones getRestrictedData y getApiData en el servicio de Data .

 .controller('RestrictedController', ['$rootScope', '$scope', 'Data', function ($rootScope, $scope, Data) { Data.getRestrictedData(function (res) { $scope.data = res.data; }, function () { $rootScope.error = 'Failed to fetch restricted content.'; }); Data.getApiData(function (res) { $scope.api = res.data; }, function () { $rootScope.error = 'Failed to fetch restricted API content.'; }); }]);

El backend es responsable de servir los datos restringidos solo si el usuario está autenticado. Esto significa que para responder con los datos restringidos, la solicitud de esos datos debe contener un JWT válido dentro de su encabezado de Authorization o cadena de consulta. Si ese no es el caso, el servidor responderá con un código de estado de error no autorizado 401.

Servicio de autenticación

El servicio de autenticación es responsable de iniciar sesión y registrar solicitudes HTTP en el backend. Si la solicitud es exitosa, la respuesta contiene el token firmado, que luego se decodifica en base64, y la información de reclamos del token adjunto se guarda en una variable tokenClaims . Esto se pasa al controlador a través de la función getTokenClaims .

 angular.module('app') .factory('Auth', ['$http', '$localStorage', 'urls', function ($http, $localStorage, urls) { function urlBase64Decode(str) { var output = str.replace('-', '+').replace('_', '/'); switch (output.length % 4) { case 0: break; case 2: output += '=='; break; case 3: output += '='; break; default: throw 'Illegal base64url string!'; } return window.atob(output); } function getClaimsFromToken() { var token = $localStorage.token; var user = {}; if (typeof token !== 'undefined') { var encoded = token.split('.')[1]; user = JSON.parse(urlBase64Decode(encoded)); } return user; } var tokenClaims = getClaimsFromToken(); return { signup: function (data, success, error) { $http.post(urls.BASE + '/signup', data).success(success).error(error) }, signin: function (data, success, error) { $http.post(urls.BASE + '/signin', data).success(success).error(error) }, logout: function (success) { tokenClaims = {}; delete $localStorage.token; success(); }, getTokenClaims: function () { return tokenClaims; } }; } ]);

Servicio de datos

Este es un servicio simple que realiza solicitudes al servidor de autenticación, así como al servidor API para algunos datos restringidos ficticios. Realiza la solicitud y delega las devoluciones de llamada de éxito y error al controlador.

 angular.module('app') .factory('Data', ['$http', 'urls', function ($http, urls) { return { getRestrictedData: function (success, error) { $http.get(urls.BASE + '/restricted').success(success).error(error) }, getApiData: function (success, error) { $http.get(urls.BASE_API + '/restricted').success(success).error(error) } }; } ]);

Más allá de este tutorial de token web JSON

La autenticación basada en tokens nos permite construir sistemas desacoplados que no están vinculados a un esquema de autenticación en particular. El token puede generarse en cualquier lugar y consumirse en cualquier sistema que use la misma clave secreta para firmar el token. Están preparados para dispositivos móviles y no requieren que usemos cookies.

Los tokens web JSON funcionan en todos los lenguajes de programación populares y están ganando popularidad rápidamente. Están respaldados por empresas como Google, Microsoft y Zendesk. Su especificación estándar del Grupo de trabajo de ingeniería de Internet (IETF) aún se encuentra en la versión preliminar y puede cambiar ligeramente en el futuro.

Todavía hay mucho que cubrir sobre los JWT, por ejemplo, cómo manejar los detalles de seguridad y cómo actualizar los tokens cuando caducan, pero el tutorial de JSON Web Token debería demostrar el uso básico y, lo que es más importante, las ventajas de usar JWT.

Lecturas adicionales en el blog de ingeniería de Toptal:

  • Creación de una API REST de Node.js/TypeScript, parte 3: MongoDB, autenticación y pruebas automatizadas