Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 7, 2022 04:40 pm GMT

[Parte 5] ASP.NET: Identity Core y JWT

Introduccin

Autenticacin con Bearer Tokens es el tema del momento y aunque ya he hablado anteriormente de eso aqu en mi blog(ASP.NET Core 6: Autenticacin JWT y Identity Core) he decidido a volver a tocar el tema.

La intencin de volver a hablar sobre JWTs es darle continuidad a la serie de posts que estamos haciendo sobre ASP.NET Core y CQRS con MediatR, ya que en temas posteriores necesitaremos tener autenticacin y autorizacin.

El camino final ser terminar con una solucin completa construida totalmente por nosotros, concepto por concepto.

El cdigo fuente de este post lo encuentras en este branch de mi github.

Autenticacin con JWT Bearer

Cuando hablamos de JWT generalmente tambin viene el tema OpenID Connect y este se vuelve ms complicado cuando se crea un Identity Server. Pero como ya lo hemos de saber, JWT es un mecanismo que se usa en OpenID Connect y podemos usarlo independientemente de cmo hacemos la autenticacin en nuestra aplicacin.

Nota : Si quieres saber a profundidad que son los JWTs, visita mi post anterior -> ASP.NET Core 6: Autenticacin JWT y Identity Core

Instalando ASP.NET Identity Core y JWT Bearer

Para comenzar con la codificacin de la autenticacin, primero necesitamos tres paquetes NuGet:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearerdotnet add package Microsoft.AspNetCore.Identitydotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore

Identity Core es este sistema de "Membership" que nos ayuda administrar usuarios, autenticacin y autorizacin. Es bien til y 100% recomendado usarlo para no reinventar la rueda.

Actualizando el DbContext

Identity Core funciona principalmente por medio de Entity Framework. En el proyecto ya contamos con un DbContext y seguiremos usando el mismo, pero hay que actualizarlo para que ahora conozca los Entities que Identity ofrece.

using MediatrValidationExample.Domain;using Microsoft.AspNetCore.Identity;using Microsoft.AspNetCore.Identity.EntityFrameworkCore;using Microsoft.EntityFrameworkCore;namespace MediatrValidationExample.Infrastructure.Persistence;public class MyAppDbContext : IdentityDbContext<IdentityUser> // <-----{    public MyAppDbContext(DbContextOptions<MyAppDbContext> options) : base(options)    { }    public DbSet<Product> Products => Set<Product>();}

Lo ms relevante aqu, es que ahora no heredamos de DbContext pero s de IdentityDbContext<TUser>.

El tipo genrico TUser representa el usuario y la clase IdentityUser es la implementacin default que Identity ofrece. En el otro post que menciono, extendemos el IdentityUser para agregar las propiedades que se necesiten, pero en este caso por simplicidad lo dejaremos con la implementacin default.

Actualizando DB

Estamos usando una base de datos SQLite, pero sin ningn problema puede ser SQL Server o cualquiera soportado por EF Core.

Para actualizar la DB tenemos que agregar su migracin correspondiente y as actualizamos:

dotnet ef migrations add AddedIdentityCore -o Infrastructure/Persistence/Migrationsdotnet ef database update

Generando JWTs

Para poder autorizar usuarios, primero hay que autenticarlos. Para eso crearemos un nuevo Command que haga la tarea:

Nota : Recuerden que la intencin de esta serie de tutoriales es seguir usando CQRS
Nota 2: El path tradicional hubiera sido Features/Auth/Command/TokenCommand.cs pero me com el Command

Autenticacin: Features -> Auth -> TokenCommand

Crearemos este comando para autenticar usuarios con usuario y contrasea.

using MediatR;using MediatrValidationExample.Exceptions;using Microsoft.AspNetCore.Identity;using Microsoft.IdentityModel.Tokens;using System.IdentityModel.Tokens.Jwt;using System.Security.Claims;using System.Text;namespace MediatrValidationExample.Features.Auth;public class TokenCommand : IRequest<TokenCommandResponse>{    public string UserName { get; set; } = default!;    public string Password { get; set; } = default!;}public class TokenCommandHandler : IRequestHandler<TokenCommand, TokenCommandResponse>{    private readonly UserManager<IdentityUser> _userManager;    private readonly IConfiguration _config;    public TokenCommandHandler(UserManager<IdentityUser> userManager, IConfiguration config)    {        _userManager = userManager;        _config = config;    }    public async Task<TokenCommandResponse> Handle(TokenCommand request, CancellationToken cancellationToken)    {        // Verificamos credenciales con Identity        var user = await _userManager.FindByNameAsync(request.UserName);        if (user is null || !await _userManager.CheckPasswordAsync(user, request.Password))        {            throw new ForbiddenAccessException();        }        var roles = await _userManager.GetRolesAsync(user);        // Generamos un token segn los claims        var claims = new List<Claim>        {            new Claim(ClaimTypes.Sid, user.Id),            new Claim(ClaimTypes.Name, user.UserName)        };        foreach (var role in roles)        {            claims.Add(new Claim(ClaimTypes.Role, role));        }        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);        var tokenDescriptor = new JwtSecurityToken(            issuer: _config["Jwt:Issuer"],            audience: _config["Jwt:Audience"],            claims: claims,            expires: DateTime.Now.AddMinutes(720),            signingCredentials: credentials);        var jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);        return new TokenCommandResponse        {            AccessToken = jwt        };    }}public class TokenCommandResponse{    public string AccessToken { get; set; } = default!;}

Citando mi post anterior:

  • Verificacin de credenciales: Utilizamos Identity de ASP.NET para guardar usuarios (tiene ms funcionalidad, pero por ahora solo usaremos esta parte) y roles. UserManager cuenta ya con muchos mtodos para manejar usuarios, sus contraseas y sus roles.
  • Generacin del JWT: Segn el listado de claims que se generaron segn el usuario autenticado, generamos el JWT. Esto es un boilerplate, siempre ser el mismo cdigo. Lo importante es ver que estamos utilizando la configuracin del appsettings, los mismos que se utilizarn para verificar el JWT al hacer solicitudes.

Nota : ForbiddAccessException es una excepcin custom que hicimos desde los primeros posts, pero apenas la estamos usando.

La configuracin agregada que se necesita:

  "Jwt": {    "Issuer": "WebApiJwt.com",    "Audience": "localhost",    "Key": "S3cr3t_K3y!.123_S3cr3t_K3y!.123"  }

Issuer y Audience realmente no tienen relevancia aqu, cuando utilizamos OpenID Connect en forma es muy importante, pero aqu por ahora solo es un requisito.

Key s es importante, es nuestro secret para encriptar de forma simtrica.

Soluciones como Identity Server o OpenIddict utilizan encriptacin asimtrica utilizando RSA y certificados, otro tema muy bueno que puedo tomar despus.

AuthController

Para exponer nuestro comando y permitir su uso, utilizaremos este Api Controller:

using MediatR;using MediatrValidationExample.Features.Auth;using Microsoft.AspNetCore.Mvc;namespace MediatrValidationExample.Controllers;[ApiController][Route("api/auth")]public class AuthController : ControllerBase{    private readonly IMediator _mediator;    public AuthController(IMediator mediator)    {        _mediator = mediator;    }    [HttpPost]    public Task<TokenCommandResponse> Token([FromBody] TokenCommand command) =>        _mediator.Send(command);}

De forma general, estamos invocando nuestro comando tal como lo hemos hecho en posts anteriores.

Configuracin final

Ya podemos generar JWTs con el cdigo que hemos escrito, pero tenemos que terminar de configurar las dependencias y decirle a Web API que utilice un esquema de autenticacin (en este caso, Bearer Tokens).

// cdigo omitido...[Authorize] // <---[ApiController][Route("api/products")]public class ProductsController : ControllerBase{// ...cdigo omitido

Usamos el atributo [Authorize] para que el controlador pida un esquema de autenticacin. ASP.NET Core admite uno o ms esquemas de autenticacin distintos. Es decir, podemos combinar JWTs con Cookie authentication o cualquier otra forma que queramos. Es comn tener solo uno, pero sin problema se podran tener dos o ms (aunque no sabra para qu, pero se puede).

La configuracin se divide en dos:

// Identity Corebuilder.Services    .AddIdentityCore<IdentityUser>()    .AddRoles<IdentityRole>()    .AddEntityFrameworkStores<MyAppDbContext>();

Aqu configuramos todas las dependencias de Identity, tanto que implementacin de TUser usar y de TRoles, tambin el contexto a utilizar.

Nota : Mencion anteriormente que Identity Core es un framework de autenticacin y de autorizacin (Claims, Roles, Policies, etc)

// Autenticacin y autorizacinbuilder.Services    .AddHttpContextAccessor()    .AddAuthorization()    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)    .AddJwtBearer(options =>    {        options.TokenValidationParameters = new TokenValidationParameters        {            ValidateIssuer = true,            ValidateAudience = true,            ValidateLifetime = true,            ValidateIssuerSigningKey = true,            ValidIssuer = builder.Configuration["Jwt:Issuer"],            ValidAudience = builder.Configuration["Jwt:Audience"],            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))        };    });

Aqu configuramos la autenticacin y autorizacin con Bearer Tokens.

Nota : Posts que te pueden interesar sobre este tema: JWT y OpenID

Actualizando Swagger

La plantilla de Web API por default agrega una configuracin bsica de Swagger. Para poder probar la autenticacin con Bearer Tokens, debemos de decirle a Swagger que debemos de ingresar un JWT en el header Authorization.

builder.Services.AddSwaggerGen(c =>{    c.SwaggerDoc("v1", new OpenApiInfo    {        Title = "My API",        Version = "v1"    });    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme    {        In = ParameterLocation.Header,        Description = "Please insert JWT with Bearer into field",        Name = "Authorization",        Type = SecuritySchemeType.ApiKey    });    c.AddSecurityRequirement(new OpenApiSecurityRequirement {   {     new OpenApiSecurityScheme     {       Reference = new OpenApiReference       {         Type = ReferenceType.SecurityScheme,         Id = "Bearer"       }      },      new string[] { }    }  });});

Esto es una receta, Swashbuckle tiene mucha ms configuracin, pero pues ese es un tema que te dejo de tarea.

Seed Users

Anteriormente ya contabamos con un mtodo Seed para datos de prueba, este mismo mtodo lo actualizamos de la siguiente forma:

async Task SeedProducts(){    using var scope = app.Services.CreateScope();    var context = scope.ServiceProvider.GetRequiredService<MyAppDbContext>();    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();    // cdigo omitido...    var testUser = await userManager.FindByNameAsync("test_user");    if (testUser is null)    {        testUser = new IdentityUser        {            UserName = "test_user"        };        await userManager.CreateAsync(testUser, "Passw0rd.1234");        await userManager.CreateAsync(new IdentityUser          {              UserName = "other_user"          }, "Passw0rd.1234");    }}

Estamos creando dos usuarios, para pruebas de autorizacin que haremos ms adelante. Mientras tanto, ya estamos listos para probar casi todo .

Probando la Autenticacin

Corremos la aplicacin y se nos abrir Swagger:
Image description
El candado Authorize es la configuracin adicional que indicamos en el Program, as swagger nos deja anexar JWTs.

Aqu solo resta que hagas pruebas, intenta consultar productos o crearlos, y no podrs por que necesitas estar autenticado con tu usuario y contrasea.

Utiliza el endpoint /api/auth/ para generar JWTs segn las credenciales que pusimos en el mtodo Seed. Utiliza el botn Authorize para agregar el JWT al header Authorization:

Image description
## Agregando Autorizacin

La autorizacin empieza por ser sencilla, pero puede complicarse. Siempre suelo hacer autorizacin basada en roles, ms si ya estoy usando Identity.

Realmente ya tenemos todo configurado, solo hay que agregar los roles a la base de datos y asignarlos a un usuario para probar.

Actualizamos nuestro mtodo Seed y agregamos lo siguiente al final:

// Cdigo omitidovar roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();var adminRole = await roleManager.FindByNameAsync("Admin");if (adminRole is null){    await roleManager.CreateAsync(new IdentityRole    {        Name = "Admin"    });    await userManager.AddToRoleAsync(testUser, "Admin");}

Aqu estamos creando un rol llamado Admin y se lo asignamos a nuestro usuario de prueba (test_user) que previamente se consult en este mtodo.

Los roles en Identity se tienen que registrar en la base de datos, como estos suelen ser fijos, es normal tenerlos en un mtodo Seed como este.

La clase IdentityRole es la implementacin default de un Rol, pero tambin yo suelo extenderlos usando herencia para agregar ms propiedades, como descripcin y categora del rol (pero bueno, aqu es segn el requerimiento).

La intencin de usar la autorizacin basada en roles, es permitir que solo usuarios con el rol Admin sean los que pueden crear productos. Por lo tanto, actualizamos el mtodo create:

  /// <summary>  /// Crea un producto nuevo  /// </summary>  /// <param name="command"></param>  /// <returns></returns>  [HttpPost]  [Authorize(Roles = "Admin")] // <----- Autorizacin por rol  public async Task<IActionResult> CreateProduct([FromBody] CreateProductCommand command)  {      await _mediator.Send(command);      return Ok();  }

Volvemos usar el atributo [Authorize] pero ahora indicando de que este mtodo necesita de un rol en particular.

Si corremos nuevamente la solucin veremos un par de cosas importantes. En el mtodo Seed se acaba de crear un AspNetRole llamado Admin y tambin se cre una relacin en AspNetUserRoles:
Image description
Image description
Al crear el JWT nosotros revisamos esta relacin. Es decir, al autenticar un usuario, consultamos los roles del usuario para anexarlos al JWT de una forma que ASP.NET entienda que son roles.

Al momento de querer autorizar usuarios, ASP.NET revisar esos Claims del JWT para verificar si tiene autorizacin de ese mtodo o no.

Puedes hacer pruebas con los dos usuarios que hemos creado: test_user cuenta con el rol Admin pero other_user no, explora con ambos usuarios para ver cmo se comporta y si la autorizacin est funcionando o no.

Con esta configuracin de autenticacin, podemos hacer cosas estilo User.IsInRole("Admin") para verificar si el usuario actual tiene cierto rol. Es muy til siempre.

Accesando al Usuario actual.

Y pues no hemos terminado.

La idea de autorizar usuarios es tambin poder saber quines son al momento de que realizan solicitudes, por lo tanto, debemos de tener un mecanismo para acceder al usuario actual.

El usuario actual se determina segn el JWT que se est mandando en la solicitud, por lo cual debemos de poder tener acceso al HttpContext.

El acceso al HttpContext se hace por medio del IHttpContextAccessor y este solo est disponible cuando existe una solicitud HTTP real.

Qu significa eso? pues cuando estemos haciendo Unit Testing, existe la posibilidad (o ms bien, es un hecho) de que no existir un HttpContext, por lo cual, este mecanismo de acceder a usuarios debe de ser una abstraccin y as poder testear en dado caso.

En futuros posts haremos Integration Tests y posiblemente Unit Tests, por lo que debemos de ser capaces de adaptarnos a esos requisitos.

Services -> ICurrentUserService

ICurrentUserService ser la abstraccin que nos permitir el acceso al usuario actual.

namespace MediatrValidationExample.Services;public interface ICurrentUserService{    CurrentUser User { get; }    bool IsInRole(string roleName);}public record CurrentUser(string Id, string UserName);

Aqu estamos definiendo el contrato que necesitamos para poder acceder al usuario actual, por ahora solo necesitamos un objeto con el ID y su UserName.

Tambin estamos abstrayendo el cmo se define si un usuario tiene un rol o no. Para Unit Testing podra ser importante hacer Mocks de esto, por ahora as lo dejamos. La idea principal es no hacer uso del HttpContext directamente, ya que este solo est disponible cuando hablamos de una aplicacin Web, pero si el da de maana necesitamos cambiar el UI y crear una aplicacin de consola (esto s me ha pasado), como por ejemplo herramientas para exportar/importar datos.

Podramos necesitar acceder a la funcionalidad de Application Core (features), pero ya no desde una aplicacin web, por eso creamos esta abstraccin.

Nota : CurrentUser podra (o debera) de ir en otro archivo, por simplicidad lo pongo junto con la definicin de la interfaz.

Su implementacin queda as:

using System.Security.Claims;namespace MediatrValidationExample.Services;public class CurrentUserService : ICurrentUserService{    private readonly IHttpContextAccessor _httpContextAccessor;    public CurrentUserService(IHttpContextAccessor httpContextAccessor)    {        _httpContextAccessor = httpContextAccessor;        var id = _httpContextAccessor.HttpContext.User.Claims            .FirstOrDefault(q => q.Type == ClaimTypes.Sid)            .Value;        var userName = _httpContextAccessor.HttpContext.User.Identity.Name;        User = new CurrentUser(id, userName);    }    public CurrentUser User { get; }    public bool IsInRole(string roleName) =>        _httpContextAccessor.HttpContext!.User.IsInRole(roleName);}

Esto es lo que estara fuertemente acoplado al HttpContext, pertenece a la presentacin directamente.

HttpContext.User es inicializado automaticamente por ASP.NET, ya que con Bearer Tokens hemos indicado que se espera un JWT en el header de Authorization. De la misma forma, con HttpContext.User.IsInRole podemos hacer la comprobacin de si el usuario actual cuenta con algn rol. Todo esto es posible porque en el JWT hemos indicado con claims, los roles que tiene el usuario.

Nota : Prximamente, probablemente dividamos esta aplicacin en distintos proyectos siguiendo un estilo Vertical Slice Architecture

Actualizando AuthController

Actualizamos el controlador de autorizacin para explicar el uso de ICurrentUserService:

    [Authorize]    [HttpGet("me")]    public IActionResult Me([FromServices] ICurrentUserService currentUser)    {        return Ok(new        {            currentUser.User,            IsAdmin = currentUser.IsInRole("Admin")        });    }

ICurrentUserService solo es usable si el usuario actual est autenticado, probablemente tendremos errores si se intenta acceder a /me si no existe un JWT .

Nota : Antes de correr el proyecto, debemos de registrar el servicio como dependencia builder.Services.AddScoped<ICurrentUserService, CurrentUserService>().

La respuesta que se obtendr al llamarlo con Swagger:

{  "user": {    "id": "308e554d-4251-47f9-9617-726dff6562ef",    "userName": "other_user"  },  "isAdmin": false}

Si probamos con el usuario Admin:

{  "user": {    "id": "f28cf715-2171-4c0e-9ba5-f2bbbb958f63",    "userName": "test_user"  },  "isAdmin": true}

Podemos ver que el mtodo IsInRole funciona sin problema.

Ya con esto definimos una abstraccin para poder acceder al usuario actual que hace la solicitud. No importa si despus decidimos cambiar de Web a CLI, el Application Core deber seguir funcionando sin problemas.

Nota: Esta parte es solo para explicar cmo se podra usar ICurrentUserService. La propiedad IsAdmin tambin es un ejemplo.

Conclusin

Hemos agregado autenticacin y autorizacin a nuestra aplicacin que hemos construido en estos 5 posts (hasta ahora) y utilizando ASP.NET Identity Core nos hemos ahorrado mucho trabajo en la cuestin de seguridad de usuarios.

No tenemos que tocar ningn algoritmo de encriptacin ni de hash para poder guardar usuarios con contraseas de manera segura. Identity Core cuenta con mucha ms funcionalidad, como generar cdigos de reinicio de contrasea o de confirmacin de correo electrnico, pero lo dejaremos para otro post.

Espero que te sea de utilidad, cualquier pregunta no dudes en contactarme en twitter y con gusto te ayudo con cualquier cosa.


Original Link: https://dev.to/isaacojeda/part-aspnet-identity-core-y-jwt-1l84

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To