Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 2, 2022 11:09 pm GMT

ASP.NET Core 6: Autenticacin JWT y Identity Core

Introduccin

En este artculo exploraremos a fondo las caractersticas de los JSON Web Tokens, su composicin y su implementacin utilizando Minimal APIs y ASP.NET Identity.

El cdigo de ejemplo lo podrs encontrar en este repositorio en mi github. Espero les sea de utilidad.

Autenticacin JWT Bearer

Qu es un Json Web Token?

Un JSON Web Token (JWT) es un estndar (RFC 7519) que define una forma segura y compacta de transmitir informacin entre dos entidades en forma de un objeto JSON.

Esta informacin puede ser verificada y es confiable ya que est firmada digitalmente. Los JWTs pueden ser firmados utilizando una llave privada (con un algoritmo HMAC) o con llaves pblicas y privadas utilizando RSA o ECDSA.

Cuando deberas utilizar Json Web Tokens?

Aqu veremos un par de escenarios donde es til y recomendable utilizar los JWTs:

  • Autorizacin: Este es el caso de uso ms comn de los JWTs. Una vez que un usuario ha iniciado sesin, cada llamada subsecuente al servicio incluir el JWT, permitiendo al usuario acceder a rutas, servicios o recursos que solo estn permitidos con su debido token. SSO (Single Sign On) es una funcionalidad que hoy en da usa los JWTs ampliamente, por que son de tamao reducido y por su habilidad de ser usado entre diferentes dominios.
  • Intercambio de Informacin: Los JWTs son tiles tambin para transmitir informacin entre dos entidades. Debido a que los JWTs pueden estar firmados por ejemplo, utilizando una llave pblica/privada podemos estar seguros que quien manda la informacin es verdaderamente l quien lo manda. Adicionalmente, la firma es calculada utilizando el encabezado del JWT y el contenido (payload) por lo que tambin estamos seguros que el contenido del JWT no fue alterado.

Qu estructura tiene un JWT?

Un JWT est separado por puntos ( . ) en tres partes, las cuales son:

  • Encabezado (header)
  • Contenido (payload)
  • Firma (signature)

Un JWT comnmente tiene la siguiente forma.

xxxxx.yyyyy.zzzzz

Veamos que significa cada una de estas partes.

Header

El encabezado tpicamente consiste de dos partes: el tipo de token (que ser JWT) y el algoritmo que se est usando en la firma, que puede ser HMAC SHA256 o RSA.

Por ejemplo:

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

Despus, este JSON se codifica en Base64URL para formar parte del primer segmento del JWT.

Payload

La segunda parte del JWT es el contenido que se transmite o certifica (payload), el cual contiene la serie de claims. Claims son afirmaciones sobre una entidad (usualmente, el usuario) e informacin adicional. Hay tres tipos de claims: registrados, pblicos y privados.

  • Claims registrados: Son un conjunto de claims predefinidos que no son obligatorios pero s recomendados, para proveer un conjunto de claims interoperables. Algunos de ellos son: iss (issuer), exp (tiempo de expiracin), sub (subject), aud (audience), entre otros.

Ntese que los nombres de los claims son de tres letras por la misma intencin de mantener el JWT de tamao reducido.

  • Claims pblicos: Estos pueden ser definidos como cada quien desee, pero para evitar colisiones de nombres y mantener un estndar (ya que puede usarse en distintos servicios), se utiliza la siguiente lista llamada IANA JSON Web Token Registry.
  • Claims privados: Estos claims son personalizados por cada quien que implemente los JWTs y al igual que los pblicos, para evitar colisiones es recomendable utilizar un formato URL con algn namespace y as asegurar que son nicos
    • Por ejemplo, un claim que guarda los roles de ASP.NET Core tendra el siguiente nombre: http://schemas.microsoft.com/ws/2008/06/identity/claims/role.

Un ejemplo de un payload sera el siguiente:

{  "sub": "1234567890",  "name": "John Doe",  "admin": true}

Y al igual que el header, este segmento se codifica en Base64Url.

Nota: Aunque los JWT estn firmados, solo estn protegidos para evitar falsificaciones (editar el payload) pero de igual forma, toda la informacin en el payload es visible para cualquiera. NO INCLUYAS informacin sensible en el payload al menos que est encriptada*.*

Signature

Para crear la firma debemos de tomar el header codificado, el payload codificado, una llave secreta, el algoritmo especificado en el header y firmar todo eso.

Por ejemplo, si vamos a utilizar el algoritmo de encripcin HMAC SHA256, la firma ser creada de la siguiente forma:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

La firma se usar para verificar que el mensaje no ha cambiado mientras viaja por la red, y en caso de ser tokens firmados por una llave privada de un certificado, tambin se puede verificar el emisor.

Juntando todo

Al final, tendremos tres cadenas de texto codificadas en Base64-URL separadas por puntos y se podrn incluir en solicitudes HTTP o contenido HTML sin ningn problema. Esto es una forma mucho ms compacta comparado a otros estndares como SAML que utiliza XML.

Al final, tendramos un JWT de la siguiente forma:

JWT

Si quieres jugar y generar tus propios JWT de prueba, puedes visitar jwt.io.

Cmo funcionan los JWT?

Cuando un usuario ha sido autenticado, el servicio deber regresar un JSON Web Token para ser usado como sus credenciales. Dado que esto es usado para autorizar el usuario, debes de considerar cuidar muy bien donde guardas el token, y eliminarlo lo ms pronto posible si ya no se requiere.

Cuando un usuario quiere acceder a contenido restringido en una ruta protegida, se debe de incluir el token en el HTTP Header Authorization y utilizando el esquema Bearer.

Ejemplo:

Authorization: Bearer <token>

Generalmente en Web APIs (y como lo haremos ms adelante) que son aplicaciones stateless, siempre requerir que el token vaya incluido en el encabezado Authorization. El servicio verificar lo necesario para determinar si es un token vlido o no, y si este es vlido. leer su informacin (los claims) y lo usar en la solicitud de ser necesario.

Esto tambin reduce las consultas a bases de datos para leer informacin del usuario, ya que el token puede contener informacin comn para poder operar (como username, email, roles, etc).

Dado que el token va incluido en el header, no habr problemas con el Cross-Origin Resource Sharing (CORS) ya que no se utilizan cookies (las cookies son por dominio).

El siguiente diagrama muestra como se podra utilizar una autorizacin y autenticacin por medio de JWT:

Image description

  1. La aplicacin cliente solicita autorizacin al Identity Server (como Auth0 o Azure AD B2C). Esto se puede hacer por medio de distintos flujos de autorizacin definidos en el estndar OpenID Connect (pero no estamos obligados a seguirlos). De igual forma, si seguimos OpenID, tpicamente se utilizara el endpoint /oauth/authorize utilizando el flujo de code flow.
  2. Cuando se autoriza el acceso, el servidor de autorizacin regresa el access token a la aplicacin cliente
  3. La aplicacin cliente usa el access token para acceder a recursos protegidos (como una API)

Y el cdigo? Probemos con ASP.NET y Minimal APIs

En este ejemplo utilizaremos herramientas production-ready y tratar de mantenerlo simple, sin embargo, cada quien podr decidir como estructurarlo e implementarlo.

Anteriormente mencionamos el estndar OpenId, que especifica como realizar estos flujos de autenticacin, pero para fines prcticos y didcticos, realizaremos nuestro propio servidor de autorizacin (ser el mismo que la API protegida) pero es muy recomendable delegar este proceso a servicios (como Auth0) o frameworks (como IdentityServer) certificados para una mayor seguridad y compliance.

En este proyecto utilizaremos:

  • Entity Framework Core con SQLite para persistencia (para fines del ejemplo, en produccin deberas de usar un servicio como SQL Azure o similares)
  • ASP.NET Identity para el manejo de credenciales.
  • Minimal APIs por su sencilles, pero podrn usar Controllers, Carter, ApiEndpoints o cualquier endpoint que deseen.

Para comenzar, crearemos un proyecto Web vaco:

dotnet new web -o WebApiJwt

Y necesitamos los siguientes paquetes registrados en el WebApiJwt.csproj:

<ItemGroup>  <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />  <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />  <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.1" />  <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />  <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />  <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>    <PrivateAssets>all</PrivateAssets>  </PackageReference></ItemGroup>

Persistencia

Crearemos una carpeta llamada Persistence y aqu pondremos las migraciones y el DbContext con tablas preestablecidas por Identity:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;using Microsoft.EntityFrameworkCore;using WebApiJwt.Entities;namespace WebApiJwt.Persistence;public class MyDbContext : IdentityDbContext<User>{    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)    {    }}

Para lo cual, necesitaremos nuestra definicin custom de la clase Usuario:

using Microsoft.AspNetCore.Identity;namespace WebApiJwt.Entities;public class User : IdentityUser{    public string FirstName { get; set; } = default!;    public string LastName { get; set; } = default!;}

Aqu estamos usando un DbContext con tablas preestablecidas y IdentityUser es parte de ellas, solo lo estamos extendiendo para agregar campos personalizados (nombre y apellidos).

Configuracin de Identity y JWT

Para configurar Identity y EntityFramework, registramos las siguientes dependencias en nuestro archivo Program.cs:

using Microsoft.AspNetCore.Authentication.JwtBearer;using Microsoft.AspNetCore.Identity;using Microsoft.AspNetCore.Mvc;using Microsoft.IdentityModel.Tokens;using System.IdentityModel.Tokens.Jwt;using System.Security.Claims;using System.Text;using WebApiJwt.Entities;using WebApiJwt.Models;using WebApiJwt.Persistence;var builder = WebApplication.CreateBuilder(args);builder.Services    .AddSqlite<MyDbContext>(builder.Configuration.GetConnectionString("Default"))    .AddIdentityCore<User>()    .AddRoles<IdentityRole>()    .AddEntityFrameworkStores<MyDbContext>();
  • AddSqlite: Registra el DbContext, es un atajo del mtodo habitual AddDbContext
  • AddIdentityCore: Registra las dependencias que necesita Identity, como generador de contraseas, manejo de usuarios, etc
  • AddRoles: Registra todo lo necesario para poder usar roles (en este caso, con la implementacin default de la clase IdentityRole)
  • AddEntityFrameworkStores: Vincula nuestro contexto de EntityFramework con todas sus dependencias que Identity necesita respecto a persistencia

Despus de esto, agregamos la configuracin que necesitamos para poder autenticar por medio de JWTs:

builder.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"]))        };    });
  • AddHttpContextAccessor: Registra el IHttpContextAccessor que nos permite acceder el HttpContextde cada solicitud (la usaremos ms adelante para acceder al usuario actual autenticado)
  • AddAutorization: Dependencias necesarias para autorizar solicitudes (como autorizacin por roles)
  • AddAuthentication: Agrega el esquema de autenticacin que queramos usar, en este caso, queremos usar por default la autenticacin por Bearer Tokens
  • AddJwtBearer: Configura la autenticacin por tokens, especificando que debe de validar y que llave privada utilizar
    • Por supuesto, esta configuracin la va a leer del appsettings.json

Quedando el archivo de configuracin de la siguiente manera:

{  "ConnectionStrings": {    "Default": "Data Source=Identity.db"  },  "Jwt": {    "Issuer": "WebApiJwt.com",    "Audience": "localhost",    "Key": "S3cr3t_K3y!.123_S3cr3t_K3y!.123"  },  "Logging": {    "LogLevel": {      "Default": "Information",      "Microsoft.AspNetCore": "Warning"    }  },  "AllowedHosts": "*"}

En este punto, deberamos de poder crear las migraciones de la base de datos (en este caso, SQLite) y actualizar el esquema con todo lo predefinido por Identity:

dotnet ef migrations add FirstMigration -o Persistence/Migrations

Y contaramos con algo similar a lo siguiente:

Image description

Para finalizar la configuracin y antes de implementar la autenticacin, debemos de usar dos middlewares que nos ayudarn a decodificar automticamente el JWT y agregarlo (en caso de ser vlido) a la solicitud HTTP.

var app = builder.Build();app.UseAuthentication();app.UseAuthorization();app.MapGet("/", () => "Hello World!");app.Run();

Endpoints

Implementaremos dos endpoints, uno para autenticacin y uno para simular un acceso restringido

Authorization endpoint (/token):

app.MapPost("/token", async (AuthenticateRequest request, UserManager<User> userManager) =>{    // Verificamos credenciales con Identity    var user = await userManager.FindByNameAsync(request.UserName);    if (user is null || !await userManager.CheckPasswordAsync(user, request.Password))    {        return Results.Forbid();    }    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),        new Claim(ClaimTypes.GivenName, $"{user.FirstName} {user.LastName}")    };    foreach (var role in roles)    {        claims.Add(new Claim(ClaimTypes.Role, role));    }    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]));    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);    var tokenDescriptor = new JwtSecurityToken(        issuer: builder.Configuration["Jwt:Issuer"],        audience: builder.Configuration["Jwt:Audience"],        claims: claims,        expires: DateTime.Now.AddMinutes(720),        signingCredentials: credentials);    var jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);    return Results.Ok(new    {        AccessToken = jwt    });});

El cdigo de arriba se divide en dos partes:

  • 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.

Por parmetro se recibe el usuario y contrasea, este es el siguiente record:

namespace WebApiJwt.Models;public record AuthenticateRequest(string UserName, string Password);

Protected endpoint (/me)

Este endpoint lo nico que har es regresar la informacin del usuario (claims) segn el JWT que se mand:

app.MapGet("/me", (IHttpContextAccessor contextAccessor) =>{    var user = contextAccessor.HttpContext.User;    return Results.Ok(new    {        Claims = user.Claims.Select(s => new        {            s.Type,            s.Value        }).ToList(),        user.Identity.Name,        user.Identity.IsAuthenticated,        user.Identity.AuthenticationType    });}).RequireAuthorization();

Utilizamos IHttpContextAccessor para acceder al usuario decodificado automticamente por el middleware y simplemente regresamos esa informacin como prueba.

Usamos la extensin RequireAuthorization para indicar al endpoint que se necesita un esquema de autorizacin y como no se especfica lo contrario, utilizar el esquema default, que es Bearer Tokens.

Probando la solucin

Para poder probar esto, necesitamos usuarios de prueba, para eso crearemos un mtodo SeedData dentro del Program.cs

async Task SeedData(){    var scopeFactory = app!.Services.GetRequiredService<IServiceScopeFactory>();    using var scope = scopeFactory.CreateScope();    var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();    var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();    var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();    context.Database.EnsureCreated();    if (!userManager.Users.Any())    {        logger.LogInformation("Creando usuario de prueba");        var newUser = new User        {            Email = "[email protected]",            FirstName = "Test",            LastName = "User",            UserName = "test.demo"        };        await userManager.CreateAsync(newUser, "[email protected]");        await roleManager.CreateAsync(new IdentityRole        {            Name = "Admin"        });        await roleManager.CreateAsync(new IdentityRole        {            Name = "AnotherRole"        });        await userManager.AddToRoleAsync(newUser, "Admin");        await userManager.AddToRoleAsync(newUser, "AnotherRole");    }}

Aqu simplemente nos aseguramos que la base de datos exista y si previamente no hay usuarios, se crearn los roles y un usuario de prueba utilizando las clases de Identity.

Los roles se pueden utilizar para autorizar endpoints segn el rol del usuario. En este ejemplo solo muestro como incluirlos en el JWT pero asp.net lo entender sin problema.

// ...Ms cdigovar app = builder.Build();await SeedData();app.UseAuthentication();app.UseAuthorization();// Ms cdigo...

Corremos la aplicacin y hacemos nuestras primeras pruebas utilizando HTTP Rest de VS Code (o puedes usar Postman o cualquier cliente http que gustes):

Solicitud:

POST {{host}}/tokenContent-Type: application/json{    "userName": "test.demo",    "password": "[email protected]"}

Respuesta:

HTTP/1.1 200 OKConnection: closeContent-Type: application/json; charset=utf-8Date: Sun, 02 Jan 2022 22:32:30 GMTServer: KestrelTransfer-Encoding: chunked{  "accessToken": "eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9zaWQiOiJhMWNhODMxZC1iMTIzLTQ0ZDgtYjViOC1iNjNlYWZiYzZlNDciLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoidGVzdC5kZW1vIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvZ2l2ZW5uYW1lIjoiVGVzdCBVc2VyIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjpbIkFub3RoZXJSb2xlIiwiQWRtaW4iXSwiZXhwIjoxNjQxMjA1OTUwLCJpc3MiOiJXZWJBcGlKd3QuY29tIiwiYXVkIjoibG9jYWxob3N0In0.CtTkO7JVmFl6ASRv1v7OuZhCrOHUy-AiMfNUzQbYByc"}

Puedes hacer pruebas con usuarios o contraseas incorrectas.

Para verificar el endpoint protegido llamamos el endpoint /me:

GET {{host}}/meContent-Type: application/jsonAuthorization: Bearer {{jwt}}

Respuesta:

HTTP/1.1 200 OKConnection: closeContent-Type: application/json; charset=utf-8Date: Sun, 02 Jan 2022 22:33:56 GMTServer: KestrelTransfer-Encoding: chunked{  "claims": [    {      "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/sid",      "value": "a1ca831d-b123-44d8-b5b8-b63eafbc6e47"    },    {      "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",      "value": "test.demo"    },    {      "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",      "value": "Test User"    },    {      "type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",      "value": "AnotherRole"    },    {      "type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",      "value": "Admin"    },    {      "type": "exp",      "value": "1641205950"    },    {      "type": "iss",      "value": "WebApiJwt.com"    },    {      "type": "aud",      "value": "localhost"    }  ],  "name": "test.demo",  "isAuthenticated": true,  "authenticationType": "AuthenticationTypes.Federation"}

Puedes hacer pruebas modificando el token manualmente desde JWT.io o modificando cualquier dato y explora como se comporta.

Conclusin

Los JSON Web Tokens se han convertido en el esquema default de autenticacin de las aplicaciones modernas. Saber como se forman y como implementarlas es un must have al disear una aplicacin web hoy en da.

El uso de asp.net Identity es la forma recomendada de emplear este mecanismo (o cualquier mecanismo de autenticacin) ya que el manejo de seguridad y contraseas a nivel cdigo ya no sera de nuestra preocupacin y utilizamos un framework enterprise ready en lugar de reinventar la rueda.

Referencias

JSON Web Token Introduction - jwt.io

Implementing JWT Authentication in ASP.NET Core 5 (codemag.com)


Original Link: https://dev.to/isaacojeda/aspnet-core-6-autenticacion-jwt-y-identity-core-170i

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