Hola, esta es la segunda parte del tutorial donde intentaremos conectar una SPA en Angular con un servicio creado en .Net core y añadir autenticación y autorización con Auth0.
Si aún no viste la primera parte puedes encontrarla aquí
En la primera parte creamos tanto el proyecto en angular como el de .Net Core y realizamos la comunicación entre ellos, pero nos quedamos con un problema de seguridad que a continuación resolveremos.
Así que comencemos.
Configurando Tenant de Auth0
Ya vamos avanzando, daré por hecho que en este momento ya cuentas con una cuenta de Auth0. Si no es así ¿Que esperas? es gratis! bueno de pende que plan elijas 😂.
https://auth0.com/
Una vez creada la cuenta nos pedirá que configuremos un Tenant, basta con asignar un nombre y una región. Dependiendo de tu criterio y de la protección general de regularización de datos elige la región que mas te guste. a continuación damos en CREATE
Listo! tenemos nuestro Tenant creado. Es momento de crear nuestra aplicación y la API.
lo primero que haremos será configurar la protección de nuestra API, para ello daremos click en APIs que se encuentra en el menu lateral dentro de Applications
Crearemos una nueva API dando click en el botón + CREATE API
En la pantalla de creación elegiremos un nombre que sea amistoso y fácil, tambien necesitamos dar un Identifier que por recomendación nos piden que sea una url, no es necesario que esta sea publica simplemente es para identificarla. Finalmente un algoritmo para generar los tokens utilizaremos en este caso RS256. Clicamos en CREATE
A continuación damos click en la API que acabamos de crear, vamos a la pestaña settings y en el apartado General Settings copiamos el Id. Lo mantendremos copiado y lo utilizaremos más adelante.
Listo hemos configurado nuestra API, ahora creemos nuestro Aplicación. Demos click en Applications que se encuentra en el menu lateral dentro de Applications
A continuación Click en +CREATE APPLICATION
Elegimos un nombre para distinguir nuestra aplicación y seleccionamos el tipo de aplicación Single Page Web Applications y damos click en CREATE.
Listo tenemos la aplicación creada, configuremos solo una cosa mas en la pantalla de nuestra nueva aplicación. Seleccionemos la pestaña de settings.
en el apartado de Application URIs agregamos la uri de nuestro cliente angular http://localhost:4200/ a los siguientes campos
- Allowed Callback URLs
- Allowed Logout URLs
- Allowed Web Origins
- Allowed Origins (CORS)
y guardamos los cambios.
al final de la pagina de configuración damos click en Advanced Settings y posteriormente en la pestaña OAuth.
En el campo Allowed APPs / APIs pegamos o escribimos el Id de la API que creamos anteriormente. Sí, el que te dije que copiaras y lo mantuvieras en tu portapapeles. Si no lo recuerdas ve a la sección APIs > Click en la API que creamos > Click en Settings. Se encuentra en la sección General Settings en el campo Id.
Demos click en SAVE CHANGES. Terminamos de configurar Auth0... por ahora.
De vuelta al código
Implementemos un sistema de login en nuestro proyecto de angular con Auth0. Para ello vamos a nuestra consola dentro da la raíz del proyecto e instalemos el sdk que nos proporciona Auth0
npm install @auth0/auth0-angular
Una vez instalado el paquete necesitamos primero recuperar el dominio
y el clientId
que registramos en la aplicación cliente en el Dashboard de Auth0. Si no las recordamos podemos encontrarlas en el menú Applications ⇒ Applications ⇒ ⇒ settings
Una vez que nos hagamos de ellas agregaremos algunas líneas de código al app.module.ts
quedando de la siguiente manera
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AuthModule } from '@auth0/auth0-angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
AuthModule.forRoot({
domain: 'Domain',
clientId: 'ClientID'
}),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Acabamos de importar al modulo principal Auth0Module y lo configuramos para que funcione con las credenciales de nuestra aplicación. Asegúrate de cambiar Domain y ClientId por los de tu aplicación SPA configurada anteriormente.
Crearemos un nuevo componente que se encargará de realizar el login y logout de nuestra aplicación
ng g c components/auth-button
El comando anterior genero un componente llamado auth-button.component
agreguemos funcionalidad a este componente
// ### auth-button.component.ts ###
import { DOCUMENT } from '@angular/common';
import { Component, Inject, OnInit } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';
@Component({
selector: 'app-auth-button',
templateUrl: './auth-button.component.html',
styleUrls: ['./auth-button.component.scss']
})
export class AuthButtonComponent implements OnInit {
constructor(@Inject(DOCUMENT) public document: Document, public auth: AuthService) {}
ngOnInit(): void {
}
}
<!-- ### auth-button.component.ts ### -->
<ng-container *ngIf="auth.isAuthenticated$ | async; else loggedOut">
<div class="profile-container">
<div class="profile" *ngIf="auth.user$ | async as user">
<img class="img-profile" *ngIf="user.picture" [src]="user.picture" alt="image profile">
{{user.name}}
</div>
<button class="btn btn-red" (click)="auth.logout({ returnTo: document.location.origin })">
Log out
</button>
</div>
</ng-container>
<ng-template #loggedOut>
<button class="btn btn-red" (click)="auth.loginWithRedirect()">Log in</button>
</ng-template>
Lo que acabamos de hacer es definir un botón que se encargara de hacer la petición a Auth0 para realizar el login del usuario mediante el método auth.loginWithRedirect()
, al dar click redirigirá al usuario al Universal Login Page de Auth0 para poder registrarlo y autenticarlo. Una vez autenticado Auth0 redireccionara a la url que definimos anteriormente [http://localhost:4200](http://localhost:4200)
.
Una vez autenticados en nuestra aplicación, podremos observar un poco de la información del usuario activo gracias al observable atuth.user$
, este observable tiene información sensible del usuario autenticado. Cabe aclarar que este observable solo emitirá su valor si isAuthenticate$
es true
a demás de la información del usuario, veremos que el botón login ha cambiado a logout.
Esto es gracias que estamos utilizando isAuthenticated$
para validar si el usuario está autenticado, ahora al hacer click en Logout para salir se llamará al método logouth()
quien se encargara de terminar la sesión del usuario. Es importante saber que le estamos diciendo al método logouth()
que redireccione a http://localhost:4200
que definimos en la sección de Allowed Logout URLs en el dashboard. Solo que estamos haciendo uso de window.location.origin
para indicar lo mismo.
Listo! Pudimos configurar el cliente para que permita el registro e inicio de sesión, pero si hacemos click en los botones para llamar a los servicios de backend notamos que funcionan aun cuando no se haya iniciado sesión. De los tres servicios que tenemos solo queremos que uno sea público, los otros dos botones no deberían retornarnos una respuesta (200OK) a menos que tengamos iniciada la sesión. Vamos a solucionar esta problemática.
Configurando autenticación en backend mediante JWT
A continuación protegeremos la API mediante la validación de un token emitido por Auth0, para eso haremos uso de una librería que nos ayudara con eso.
La libreria en cuestion es Microsoft.AspNetCore.Authentication.JwtBearer
y la podemos instalar de 2 formas distintitas. La primera mediante el administrador de paquetes nugget en Visual Studio y la otra con el CLI de .Net.
Para instalarla con el CLI de .NET basta con ir a la rais de nuestro proyecto de .Net y ejecutar mediante la consola lo siguiente
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
La otra opción es ir al Administrador de paquetes.
En Visual Studio dar click derecho sobre el proyecto > Administrar Paquetes Nuget...
Se abrirá una pestaña dar click en examinar y buscar el paquete por su nombre, posteriormente dar click en instalar y aceptar las dependencias y licencias.
// ### TestController.ts ###
// Código existente...
public class TestController : ControllerBase
{
[HttpGet("public")]
public IActionResult GetPublic()
{
var result = new Result("Se llamó al servicio publico de manera satisfactoria.!");
return Ok(result);
}
[Authorize]
[HttpGet("private")]
public IActionResult GetPrivate()
{
var result = new Result("Se llamó al servicio privado de manera satisfactoria.!");
return Ok(result);
}
[Authorize]
[HttpGet("permission")]
public IActionResult GetPermission()
{
var result = new Result("Se llamó al servicio privado con permisos de manera satisfactoria.!");
return Ok(result);
}
}
// Código existente...
Ejecutamos la solución y probamos los servicios en postman o tu entorno de desarrollo de APIs y pruebas favorito.
Notamos como el estatus de la respuesta es un 401 Lo que significa que no tiene autorización para ver este contenido.
Excelente, hemos conseguido poner seguridad y evitar el acceso a usuarios no autorizados. Ahora probemos con nuestra aplicación en angular.
Si haz iniciado sesión, es momento de que hagas logout y finalices la sesión del usuario. Si es así prueba a pulsar los botones a ver que pasa.
Muy bien, observamos que esta aplicada la seguridad en nuestros endpoints. Ahora inicia sesión y prueba nuevamente.
🤔 Umm... Parece que algo no esta bien, deberíamos tener acceso al menos a la api privada ya que estamos logueados.
¿Que Pasó?
JWT (Json Web Token) eso es lo que pasó. ¿O no pasó?.
No voy a profundizar mucho en lo que es JWT y prometo hacer un articulo dedicado a eso, pero en pocas palabras un JWT es un JSON codificado en Base 64 que emite un servidor para verificar la identidad de un usuario que intenta acceder a un recurso. Cuando un usuario quiere acceder a un recurso mediante el protocolo HTTP debe añadir en su petición ese JWT codificado mediante la autenticación bearer. es una cadena mas o menos así:
bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1GS0hoVGdJdDVKZUdxLWtWLXZOcCJ9.eyJpc3MiOiJodHRwczovL2hlYWRsZXNzLWFwaS51cy5hdXRoMC5jb20vIiwic3ViIjoiYXV0aDB8NjA1Y2JjYjJlZDNlMjgwMDZmNmQyZTI2IiwiYXVkIjpbImh0dHA6Ly9tb25nb2RlYmFwaS5jb20iLCJodHRwczovL2hlYWRsZXNzLWFwaS51cy5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNjE2Njk0NzkzLCJleHAiOjE2MTY3ODExOTMsImF6cCI6Ijl1S3A0T2dvdkRLQTBZM2E3eDV1MXdhWDhvcGpDa3dtIiwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsImd0eSI6InBhc3N3b3JkIn0.SuL3hCo9mKM8SwV-CsRwRkP7jHL6_26-wCO4gzIjyhV89356dugoFtPa_hRPfO2HyT8tyrGI2SCytdS8tcsbqjpLDbZ8AO2b1TbJgt1Wedq8dPgF1uYsmHp-VrKOdFbQK2824kNzBVuOPEzSkgX7v2KQdBtyAlHcyuEMjaT7sSqxeq0acosJGeJ7pvYyz-Tsy6AcgNVOst3RZNrwYoqzNcBey__53HqyEFku-cblpVuMBOqq1snDgdq24rxjRHnVWNvxqwmDWu1wIwyNHzAUnEoJttHJB84aZtX8XAZvQ9zG1QHLfxwB4sB87JQz2UY1jgWxeio0hvBe0OqR_nGgxw
una vez que el servidor de recursos api recibe una petición, revisa que en las cabeceras venga ese token, de ser así lo valida con el servidor que lo emitió, si el Token es valido, no fue alterado ni ha expirado, el servidor de recursos Api permite que la petición pase, de lo contrario no lo hace y retorna un error 401 Unauthorize
.
De vuelta al proyecto de angular. Si recordamos, en ningún momento estamos recuperando ese token ni tampoco lo hemos incluido en las peticiones a las api que estamos llamando. Afortunadamente Auth0 también provee una solución sensilla de configurar un interceptor así que solucionemos eso.
// ### app.module ###
// Código existente...
import { AuthHttpInterceptor, AuthModule } from '@auth0/auth0-angular';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
// Código existente...
AuthModule.forRoot({
// Código existente...
// configuración del interceptor
httpInterceptor: {
allowedList: [{
uri: 'https://localhost:44386/api/v2/*',
tokenOptions: {
audience: 'audience asignada a tu api en el dashboard de Auth0',
}
},
{uri:'https://localhost:44386/api/test/private'},
{uri:'https://localhost:44386/api/test/permission'}
]
}
// termina configuración del interceptor
}),
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthHttpInterceptor, multi: true }
],
// Código existente...
Agregamos una configuración al array de los providers y le decimos que configure como de tipo HTTP_INTERCEPTOR
el cual proviene de AutHttpInterceptor
asegúrate de agregar la clase a tus imports.
dentro de la configuración de AuthModule
agregamos un interceptor donde indicamos la lista de sitios que funcionaran con este interceptor, aqui solo hemos configurado uno y le indicamos que le añada el token a las urls que coincidan con la uri indicada. En la primera regla que puse como ejemplo, le decimos en este caso que apunte a las url de nuestro backend. Observa como finaliza con * el cual es un comodín que le indica que todas las urls que inicien con lo que esta antes de el * deberá añadir el token.
ESTA REGLA NO LA VAMOS A EMPLEAR EN ESTE TUTORIAL solo la puse para que sepas que puedes forzar la inclusion de todas las url que coincidan con un patron.
Añadiremos únicamente las url que queremos proteger, por seguridad siempre es recomendable mandar el token a las urls solo cuando es necesario, si una api es publica no deberíamos de mandar el encabezado con un token.
Existe otro modo de trabajar con estas reglas pero por ahora solo veremos esta forma.
Antes de continuar modificaremos la política de seguridad en nuestro servidor backend para que acepte el encabezado de autorización de lo contrario recibiremos un error de cross origins.
En la clase Startup.cs
modificamos el middleware AddCors()
para que quede de la siguiente forma
// ### Startup.cs ###
// Código existente ..
services.AddCors(options =>
{
options.AddPolicy(name: AngularClient, builder =>
{
builder.WithOrigins("https://localhost:4200", "http://localhost:4200");
builder.WithHeaders("authorization");
});
});
// Código existente ...
Una vez realizados estos cambios, probemos nuestra aplicación de dos maneras, sin sesión y con una sesión activa.
Observemos que cuando hacemos las peticiones a los servicios cuando la sesión no esta iniciada, llama de manera satisfactoria a nuestro servicio publico. Sin embargo cuando queremos llamar los servicios protegidos, el interceptor se encarga de decirnos que para poder utilizar esos servicios requerimos tener una sesión iniciada, ya que si no inicia sesión no existe un token para ser añadido a la cabecera de la petición.
Si notamos en la herramientas de desarrolladores la petición hacia los servicios private y permission ni siquiera fueron enviadas.
Con sesión activa
Por otro Lado cuando la sesión está iniciada, AuthModule
puede generar un token y enviarlo en los request que definimos en la configuración. Por lo tanto obtenemos una respuesta 200 OK en cada uno de nuestros servicios solicitados.
Vamos a dejarlo por hoy terminemos para una tercer entrada la finalización de este tutorial y que cada vez se pone mas interezante.
Nos metermos de lleno a configurar y crear las politicas de autorización para permitir acciones a ciertos usuarios que cumplan con un cierto rol.
Así que hasta la otra.
Top comments (0)