Cómo hacer la autenticación JWT con un Angular 6 SPA
Publicado: 2022-03-11Hoy veremos lo fácil que es integrar la autenticación de token web JSON (JWT) en su aplicación de página única (SPA) de Angular 6 (o posterior). Comencemos con un poco de historia.
¿Qué son los tokens web JSON y por qué usarlos?
La respuesta más fácil y concisa aquí es que son convenientes, compactos y seguros. Veamos esas afirmaciones en detalle:
- Conveniente : el uso de un JWT para la autenticación en el back-end una vez que se haya iniciado sesión requiere configurar un encabezado HTTP, una tarea que se puede automatizar fácilmente a través de una función o subclase, como veremos más adelante.
- Compacto : un token es simplemente una cadena codificada en base64, que contiene algunos campos de encabezado y una carga útil si es necesario. El JWT total suele ser inferior a 200 bytes, incluso si está firmado.
- Seguro : si bien no es obligatorio, una gran característica de seguridad de JWT es que los tokens se pueden firmar mediante el cifrado de par de claves pública/privada RSA o el cifrado HMAC mediante un secreto compartido. Esto asegura el origen y la validez de un token.
Todo esto se reduce a que tiene una forma segura y eficiente de autenticar a los usuarios y luego verificar las llamadas a los puntos finales de su API sin tener que analizar ninguna estructura de datos ni implementar su propio cifrado.
Teoría de la aplicación
Entonces, con un poco de información, ahora podemos sumergirnos en cómo funcionaría esto en una aplicación real. Para este ejemplo, supondré que tenemos un servidor Node.js que aloja nuestra API, y estamos desarrollando una lista de tareas pendientes de SPA usando Angular 6. Trabajemos también con esta estructura de API:
-
/auth
→POST
(publicar nombre de usuario y contraseña para autenticar y recibir un JWT) -
/todos
→GET
(devuelve una lista de elementos de la lista de tareas para el usuario) -
/todos/{id}
→GET
(devuelve un elemento específico de la lista de tareas pendientes) -
/users
→GET
(devuelve una lista de usuarios)
Veremos la creación de esta sencilla aplicación en breve, pero por ahora, concentrémonos en la interacción en teoría. Tenemos una página de inicio de sesión simple, donde el usuario puede ingresar su nombre de usuario y contraseña. Cuando se envía el formulario, envía esa información al punto final /auth
. El servidor de nodo puede entonces autenticar al usuario de la manera que sea adecuada (búsqueda en la base de datos, consulta de otro servicio web, etc.) pero, en última instancia, el punto final debe devolver un JWT.
El JWT para este ejemplo contendrá algunos reclamos reservados y algunos reclamos privados . Los reclamos reservados son simplemente pares clave-valor recomendados por JWT que se usan comúnmente para la autenticación, mientras que los reclamos privados son pares clave-valor aplicables solo a nuestra aplicación:
Reclamos reservados
-
iss
: Emisor de este token. Por lo general, el FQDN del servidor, pero puede ser cualquier cosa siempre que la aplicación cliente sepa que lo espera. -
exp
: fecha y hora de caducidad de este token. Esto es en segundos desde la medianoche del 01 de enero de 1970 GMT (hora de Unix). -
nbf
: no válido antes de la marca de tiempo. No se usa con frecuencia, pero proporciona un límite inferior para la ventana de validez. Mismo formato queexp
.
Reclamos privados
-
uid
: ID de usuario del usuario que ha iniciado sesión. -
role
: Rol asignado al usuario que ha iniciado sesión.
Nuestra información estará codificada en base64 y firmada usando HMAC con la clave compartida todo-app-super-shared-secret
. A continuación se muestra un ejemplo de cómo se ve el JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b2RvYXBpIiwibmJmIjoxNDk4MTE3NjQyLCJleHAiOjE0OTgxMjEyNDIsInVpZCI6MSwicm9sZSI6ImFkbWluIn0.ZDz_1vcIlnZz64nSM28yA1s-4c_iw3Z2ZtP-SgcYRPQ
Esta cadena es todo lo que necesitamos para asegurarnos de que tenemos un inicio de sesión válido, para saber qué usuario está conectado e incluso qué rol(es) tiene el usuario.
La mayoría de las bibliotecas y aplicaciones proceden a almacenar este JWT en localStorage
o sessionStorage
para una fácil recuperación, pero esto es solo una práctica común. Lo que haga con el token depende de usted, siempre que pueda proporcionarlo para futuras llamadas a la API.
Ahora, siempre que el SPA desee realizar una llamada a cualquiera de los puntos finales de API protegidos, simplemente debe enviar el token en el encabezado HTTP de Authorization
.
Authorization: Bearer {JWT Token}
Nota : Una vez más, esto es simplemente una práctica común. JWT no prescribe ningún método particular para enviarse al servidor. También puede agregarlo a la URL o enviarlo en una cookie.
Una vez que el servidor recibe el JWT, puede decodificarlo, garantizar la coherencia con el secreto compartido de HMAC y verificar la caducidad con los campos exp
y nbf
. También podría usar el campo iss
para asegurarse de que era la parte emisora original de este JWT.
Una vez que el servidor está satisfecho con la validez del token, se puede usar la información almacenada dentro del JWT. Por ejemplo, el uid
que incluimos nos da la identificación del usuario que realiza la solicitud. Para este ejemplo en particular, también incluimos el campo de role
, que nos permite tomar decisiones sobre si el usuario debería poder acceder a un punto final en particular o no. (Ya sea que confíe en esta información, o más bien desee realizar una búsqueda en la base de datos, depende del nivel de seguridad requerido).
function getTodos(jwtString) { var token = JWTDecode(jwtstring); if( Date.now() < token.nbf*1000) { throw new Error('Token not yet valid'); } if( Date.now() > token.exp*1000) { throw new Error('Token has expired'); } if( token.iss != 'todoapi') { throw new Error('Token not issued here'); } var userID = token.uid; var todos = loadUserTodosFromDB(userID); return JSON.stringify(todos); }
Construyamos una aplicación Todo simple
Para continuar, deberá tener instalada una versión reciente de Node.js (6.x o posterior), npm (3.x o posterior) y angular-cli. Si necesita instalar Node.js, que incluye npm, siga las instrucciones aquí. Luego angular-cli
se puede instalar usando npm
(o yarn
, si lo ha instalado):
# installation using npm npm install -g @angular/cli # installation using yarn yarn global add @angular/cli
No entraré en detalles sobre el modelo estándar de Angular 6 que usaremos aquí, pero para el siguiente paso, creé un repositorio de Github para contener una pequeña aplicación de tareas para ilustrar la simplicidad de agregar la autenticación JWT a su aplicación. Simplemente clónalo usando lo siguiente:
git clone https://github.com/sschocke/angular-jwt-todo.git cd angular-jwt-todo git checkout pre-jwt
El comando git checkout pre-jwt
cambia a una versión con nombre en la que no se ha implementado JWT.
Debería haber dos carpetas dentro llamadas server
y client
. El servidor es un servidor API de nodo que albergará nuestra API básica. El cliente es nuestra aplicación Angular 6.
El servidor API de nodo
Para comenzar, instale las dependencias e inicie el servidor API.
cd server # installation using npm npm install # or installation using yarn yarn node app.js
Debería poder seguir estos enlaces y obtener una representación JSON de los datos. Solo por ahora, hasta que tengamos la autenticación, hemos codificado el punto final /todos
para devolver las tareas para userID=1
:
- http://localhost:4000: Página de prueba para ver si el servidor Node se está ejecutando
- http://localhost:4000/api/users: Devuelve la lista de usuarios en el sistema
- http://localhost:4000/api/todos: Devuelve la lista de tareas para
userID=1
La aplicación angular
Para comenzar con la aplicación cliente, también debemos instalar las dependencias e iniciar el servidor de desarrollo.
cd client # using npm npm install npm start # using yarn yarn yarn start
Nota : Dependiendo de la velocidad de su línea, puede tomar un tiempo descargar todas las dependencias.
Si todo va bien, ahora debería ver algo como esto al navegar a http://localhost:4200:
Agregar autenticación a través de JWT
Para agregar compatibilidad con la autenticación JWT, utilizaremos algunas bibliotecas estándar disponibles que lo simplifican. Por supuesto, puede renunciar a estas comodidades e implementar todo usted mismo, pero eso está más allá de nuestro alcance aquí.
Primero, instalemos una biblioteca en el lado del cliente. Está desarrollado y mantenido por Auth0, que es una biblioteca que le permite agregar autenticación basada en la nube a un sitio web. El uso de la biblioteca en sí no requiere que use sus servicios.
cd client # installation using npm npm install @auth0/angular-jwt # installation using yarn yarn add @auth0/angular-jwt
Llegaremos al código en un segundo, pero mientras estamos en eso, configuremos también el lado del servidor. Usaremos las bibliotecas body-parser
, jsonwebtoken
y express-jwt
para que Node comprenda los cuerpos JSON POST y los JWT.
cd server # installation using npm npm install body-parser jsonwebtoken express-jwt # installation using yarn yarn add body-parser jsonwebtoken express-jwt
Punto final de la API para la autenticación
Primero, necesitamos una forma de autenticar a los usuarios antes de darles un token. Para nuestra demostración simple, simplemente configuraremos un punto final de autenticación fijo con un nombre de usuario y una contraseña codificados. Esto puede ser tan simple o tan complejo como lo requiera su aplicación. Lo importante es devolver un JWT.
En server/app.js
agregue una entrada debajo de las otras líneas require
de la siguiente manera:
const bodyParser = require('body-parser'); const jwt = require('jsonwebtoken'); const expressJwt = require('express-jwt');
Así como lo siguiente:
app.use(bodyParser.json()); app.post('/api/auth', function(req, res) { const body = req.body; const user = USERS.find(user => user.username == body.username); if(!user || body.password != 'todo') return res.sendStatus(401); var token = jwt.sign({userID: user.id}, 'todo-app-super-shared-secret', {expiresIn: '2h'}); res.send({token}); });
Esto es principalmente código JavaScript básico. Obtenemos el cuerpo JSON que se pasó al punto final /auth
, buscamos un usuario que coincida con ese nombre de usuario, comprobamos que tenemos un usuario y la contraseña coinciden, y de lo contrario devolvemos un error HTTP 401 Unauthorized
.
La parte importante es la generación de tokens, y la desglosaremos en sus tres parámetros. La sintaxis de sign
es la siguiente: jwt.sign(payload, secretOrPrivateKey, [options, callback])
, donde:
-
payload
es un objeto literal de pares clave-valor que le gustaría codificar dentro de su token. Esta información puede ser decodificada del token por cualquier persona que tenga la clave de descifrado. En nuestro ejemplo, codificamos eluser.id
para que cuando recibamos el token nuevamente en el back-end para la autenticación, sepamos con qué usuario estamos tratando. -
secretOrPrivateKey
es una clave secreta compartida de cifrado HMAC (esto es lo que hemos usado en nuestra aplicación, por simplicidad) o una clave privada de cifrado RSA/ECDSA. -
options
representa una variedad de opciones que se pueden pasar al codificador en forma de pares clave-valor. Por lo general, al menos especificamosexpiresIn
(se convierte en un reclamo reservado deexp
) y elissuer
(reclamación reservadaiss
) para que un token no sea válido para siempre, y el servidor puede verificar que, de hecho, emitió el token originalmente. -
callback
de llamada es una función para llamar después de que se realiza la codificación, en caso de que se desee manejar la codificación del token de forma asíncrona.
(También puede leer más detalles sobre options
y cómo usar la criptografía de clave pública en lugar de una clave secreta compartida).
Integración angular 6 JWT
Hacer que Angular 6 funcione con nuestro JWT es bastante simple usando angular-jwt
. Simplemente agregue lo siguiente a client/src/app/app.modules.ts
:
import { JwtModule } from '@auth0/angular-jwt'; // ... export function tokenGetter() { return localStorage.getItem('access_token'); } @NgModule({ // ... imports: [ BrowserModule, AppRoutingModule, HttpClientModule, FormsModule, // Add this import here JwtModule.forRoot({ config: { tokenGetter: tokenGetter, whitelistedDomains: ['localhost:4000'], blacklistedRoutes: ['localhost:4000/api/auth'] } }) ], // ... }
Eso es básicamente todo lo que se requiere. Por supuesto, tenemos más código para agregar para realizar la autenticación inicial, pero la biblioteca angular-jwt
se encarga de enviar el token junto con cada solicitud HTTP.

- La función
tokenGetter()
hace exactamente lo que dice, pero la forma en que se implementa depende completamente de usted. Hemos optado por devolver el token que guardamos enlocalStorage
. Por supuesto, puede proporcionar cualquier otro método que desee, siempre que devuelva la cadena codificada del token web JSON . - La opción
whiteListedDomains
existe para que pueda restringir a qué dominios se envía el JWT, de modo que las API públicas no reciban su JWT también. - La opción
blackListedRoutes
le permite especificar rutas específicas que no deberían recibir el JWT incluso si están en un dominio incluido en la lista blanca. Por ejemplo, el extremo de autenticación no necesita recibirlo porque no tiene sentido: el token suele ser nulo cuando se llama de todos modos.
Hacer que todo funcione junto
En este punto, tenemos una manera de generar un JWT para un usuario determinado usando el punto final /auth
en nuestra API, y tenemos la plomería hecha en Angular para enviar un JWT con cada solicitud HTTP. Genial, pero podría señalar que absolutamente nada ha cambiado para el usuario. Y estarías en lo cierto. Todavía podemos navegar a cada página de nuestra aplicación y podemos llamar a cualquier punto final de la API sin siquiera enviar un JWT. ¡No es bueno!
Necesitamos actualizar nuestra aplicación de cliente para preocuparnos por quién está conectado y también actualizar nuestra API para requerir un JWT. Empecemos.
Necesitaremos un nuevo componente Angular para iniciar sesión. En aras de la brevedad, mantendré esto lo más simple posible. También necesitaremos un servicio que maneje todos nuestros requisitos de autenticación y un protector angular para proteger las rutas que no deberían ser accesibles antes de iniciar sesión. Haremos lo siguiente en el contexto de la aplicación del cliente.
cd client ng g component login --spec=false --inline-style ng g service auth --flat --spec=false ng g guard auth --flat --spec=false
Esto debería haber generado cuatro nuevos archivos en la carpeta del client
:
src/app/login/login.component.html src/app/login/login.component.ts src/app/auth.service.ts src/app/auth.guard.ts
A continuación, debemos proporcionar el servicio de autenticación y protección para nuestra aplicación. Actualice client/src/app/app.modules.ts
:
import { AuthService } from './auth.service'; import { AuthGuard } from './auth.guard'; // ... providers: [ TodoService, UserService, AuthService, AuthGuard ],
Y luego actualice el enrutamiento en client/src/app/app-routing.modules.ts
para hacer uso de la protección de autenticación y proporcionar una ruta para el componente de inicio de sesión.
// ... import { LoginComponent } from './login/login.component'; import { AuthGuard } from './auth.guard'; const routes: Routes = [ { path: 'todos', component: TodoListComponent, canActivate: [AuthGuard] }, { path: 'users', component: UserListComponent, canActivate: [AuthGuard] }, { path: 'login', component: LoginComponent}, // ...
Finalmente, actualice client/src/app/auth.guard.ts
con el siguiente contenido:
import { Injectable } from '@angular/core'; import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { constructor(private router: Router) { } canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (localStorage.getItem('access_token')) { return true; } this.router.navigate(['login']); return false; } }
Para nuestra aplicación de demostración, simplemente verificamos la existencia de un JWT en el almacenamiento local. En las aplicaciones del mundo real, descodificaría el token y verificaría su validez, vencimiento, etc. Por ejemplo, podría usar JwtHelperService para esto.
En este punto, nuestra aplicación Angular ahora siempre lo redirigirá a la página de inicio de sesión ya que no tenemos forma de iniciar sesión. Rectifiquemos eso, comenzando con el servicio de autenticación en client/src/app/auth.service.ts
:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Injectable() export class AuthService { constructor(private http: HttpClient) { } login(username: string, password: string): Observable<boolean> { return this.http.post<{token: string}>('/api/auth', {username: username, password: password}) .pipe( map(result => { localStorage.setItem('access_token', result.token); return true; }) ); } logout() { localStorage.removeItem('access_token'); } public get loggedIn(): boolean { return (localStorage.getItem('access_token') !== null); } }
Nuestro servicio de autenticación tiene solo dos funciones, login
y logout
:
-
login
POST
envía el nombre deusername
ypassword
proporcionados a nuestro back-end y estableceaccess_token
enlocalStorage
si recibe uno. En aras de la simplicidad, no hay manejo de errores aquí. -
logout
simplemente borraaccess_token
delocalStorage
, lo que requiere que se adquiera un nuevo token antes de que se pueda acceder de nuevo a cualquier otra cosa. -
loggedIn
es una propiedad booleana que podemos usar rápidamente para determinar si el usuario ha iniciado sesión o no.
Y por último, el componente de inicio de sesión. Estos no tienen relación con el trabajo real con JWT, así que siéntase libre de copiar y pegar en client/src/app/login/login.components.html
:
<h4 *ngIf="error">{{error}}</h4> <form (ngSubmit)="submit()"> <div class="form-group col-3"> <label for="username">Username</label> <input type="text" name="username" class="form-control" [(ngModel)]="username" /> </div> <div class="form-group col-3"> <label for="password">Password</label> <input type="password" name="password" class="form-control" [(ngModel)]="password" /> </div> <div class="form-group col-3"> <button class="btn btn-primary" type="submit">Login</button> </div> </form>
Y client/src/app/login/login.components.ts
necesitará:
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../auth.service'; import { Router } from '@angular/router'; import { first } from 'rxjs/operators'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent { public username: string; public password: string; public error: string; constructor(private auth: AuthService, private router: Router) { } public submit() { this.auth.login(this.username, this.password) .pipe(first()) .subscribe( result => this.router.navigate(['todos']), err => this.error = 'Could not authenticate' ); } }
Voila, nuestro ejemplo de inicio de sesión de Angular 6:
En esta etapa, deberíamos poder iniciar sesión (usando jemma
, paul
o sebastian
con la contraseña todo
) y ver todas las pantallas nuevamente. Pero nuestra aplicación muestra los mismos encabezados de navegación y no hay forma de cerrar la sesión independientemente del estado actual. Arreglemos eso antes de pasar a arreglar nuestra API.
En client/src/app/app.component.ts
, reemplace todo el archivo con lo siguiente:
import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from './auth.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { constructor(private auth: AuthService, private router: Router) { } logout() { this.auth.logout(); this.router.navigate(['login']); } }
Y para client/src/app/app.component.html
reemplace la sección <nav>
con lo siguiente:
<nav class="nav nav-pills"> <a class="nav-link" routerLink="todos" routerLinkActive="active" *ngIf="auth.loggedIn">Todo List</a> <a class="nav-link" routerLink="users" routerLinkActive="active" *ngIf="auth.loggedIn">Users</a> <a class="nav-link" routerLink="login" routerLinkActive="active" *ngIf="!auth.loggedIn">Login</a> <a class="nav-link" (click)="logout()" href="#" *ngIf="auth.loggedIn">Logout</a> </nav>
Hemos hecho que nuestra navegación tenga en cuenta el contexto de que solo debe mostrar ciertos elementos dependiendo de si el usuario ha iniciado sesión o no. auth.loggedIn
puede, por supuesto, usarse en cualquier lugar donde pueda importar el servicio de autenticación.
Asegurar la API
Usted puede estar pensando, esto es genial... todo parece estar funcionando maravillosamente . Pero intente iniciar sesión con los tres nombres de usuario diferentes y notará algo: todos devuelven la misma lista de tareas pendientes. Si echamos un vistazo a nuestro servidor API, podemos ver que cada usuario, de hecho, tiene su propia lista de elementos, entonces, ¿qué pasa?
Bueno, recuerde que cuando comenzamos, codificamos nuestro punto final de la API /todos
para que siempre devolviera la lista de tareas pendientes para userID=1
. Esto se debió a que no teníamos forma de saber quién era el usuario que había iniciado sesión actualmente.
Ahora lo hacemos, así que veamos qué tan fácil es proteger nuestros puntos finales y usar la información codificada en el JWT para proporcionar la identidad de usuario requerida. Inicialmente, agregue esta línea a su archivo server/app.js
justo debajo de la última llamada app.use()
:
app.use(expressJwt({secret: 'todo-app-super-shared-secret'}).unless({path: ['/api/auth']}));
Usamos el middleware express-jwt
, le decimos cuál es el secreto compartido y especificamos una matriz de rutas para las que no debería requerir un JWT. Y eso es. No es necesario tocar todos y cada uno de los puntos finales, crear declaraciones if
por todas partes, ni nada.
Internamente, el middleware está haciendo algunas suposiciones. Por ejemplo, asume que el encabezado HTTP de Authorization
sigue el patrón JWT común de Bearer {token}
. (Sin embargo, la biblioteca tiene muchas opciones para personalizar su funcionamiento si ese no es el caso. Consulte Uso de express-jwt para obtener más detalles).
Nuestro segundo objetivo es utilizar la información codificada JWT para averiguar quién está realizando la llamada. Una vez más express-jwt
viene al rescate. Como parte de la lectura del token y su verificación, establece la carga útil codificada que enviamos en el proceso de firma a la variable req.user
en Express. Luego podemos usarlo para acceder inmediatamente a cualquiera de las variables que almacenamos. En nuestro caso, establecemos el ID de usuario igual al ID del usuario autenticado y, como tal, podemos req.user.userID
userID
Actualice server/app.js
nuevamente y cambie el punto final /todos
para que diga lo siguiente:
res.send(getTodos(req.user.userID));
Y eso es. Nuestra API ahora está protegida contra el acceso no autorizado y podemos determinar con seguridad quién es nuestro usuario autenticado en cualquier punto final. Nuestra aplicación de cliente también tiene un proceso de autenticación simple, y cualquier servicio HTTP que escribamos que llame a nuestro punto final de API tendrá automáticamente un token de autenticación adjunto.
Si clonó el repositorio de Github y simplemente quiere ver el resultado final en acción, puede consultar el código en su forma final usando:
git checkout with-jwt
Espero que haya encontrado útil este tutorial para agregar la autenticación JWT a sus propias aplicaciones de Angular. ¡Gracias por leer!