0

Puntos de conexión de la API de identidad #2

Ejemplo en una aplicación

En este segmento, procederé a construir una aplicación de ejemplo, incorporaré todos los paquetes y servicios esenciales y, al final, estableceré las API de identidad. Según mi análisis, actualmente, las plantillas dotnet new no incluyen las API de identidad.

La plantilla webapi ofrece la opción –auth, pero en la actualidad, su compatibilidad se limita a Azure AD, Azure AD B2C o autenticación de Windows. Anticipo una futura actualización de esta plantilla para incluir la opción Individual mediante la implementación de las API de identidad en .NET 8.

Comenzaremos creando un archivo nuevo con dotnet new webapi. He utilizado el SDK de .NET 8 para todas las directrices mencionadas en este artículo.

dotnet new webapi

Para agregar las API de identidad necesitamos hacer varias cosas:

  • Agregue los paquetes necesarios
  • Adición de EF Core
  • Agregue los modelos de Identity EF Core necesarios y genere migraciones
  • Adición de las API y los servicios de identidad
  • Adición de servicios de autorización y middleware

Seguiré cada uno de esos pasos en las siguientes secciones.

Agregar de los paquetes de EF Core

Comenzaremos incorporando EF Core a nuestra aplicación, un paso que involucra la inclusión de varios paquetes.

Para esta exhibición, elegiré SQLite como la base de datos de respaldo debido a su simplicidad. Aunque no es recomendable para implementaciones en producción de aplicaciones web, es adecuado para pruebas como la que estamos llevando a cabo.

Iniciaremos la inclusión de los paquetes necesarios.

# The main package for SQLite EF Core support
dotnet add package Microsoft.EntityFrameworkCore.SQLite --prerelease

# Contains shared build-time components for EF Core
dotnet add package Microsoft.EntityFrameworkCore.Design --prerelease

# The ASP.NET Core Identity integration for EF Core
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --prerelease

He utilizado la marca –prerelease para asegurarse de obtener los paquetes más recientes (los paquetes de .NET 8). También deberás instalar la herramienta ef si aún no lo has hecho. Personalmente, actualicé la herramienta a la versión más reciente (versión de .NET 8) de la siguiente manera:

dotnet tool update --global dotnet-ef --prerelease

En este momento, contamos con todas las herramientas y paquetes necesarios, por lo tanto, incorporaremos EF Core a nuestra aplicación.

Configuración de EF Core en la aplicación

Para iniciar la integración con EF Core, es necesario contar con una implementación de DbContext en nuestra aplicación. La implementación más elemental de DbContext se presenta de la siguiente manera (es importante destacar que la ajustaremos pronto para ofrecer soporte a Identity).

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
     : base(options)
    {
    }
}

Podemos registrarnos AppDbContext  en nuestra aplicación llamando AddDbContext<> a en WebApplicationBuilder :

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add EF Core
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));

// ...

En la porción de código mencionada anteriormente, hemos ajustado la configuración de EF Core para que emplee nuestra clase AppDbContext, optando por utilizar SQLite y especificando una cadena de conexión denominada DefaultConnection. La definición de la cadena de conexión puede llevarse a cabo en appsettings.json (cabe mencionar que no he expuesto la configuración previa, solo la nueva entrada):

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=my_test_app.db"
  }
}

Eso está configurado con EF Core, por lo que ahora podemos configurar ASP.NET identidad de Core.

Agregar Identity y los servicios de punto de conexión de la API de identidad

Antes de incorporar los servicios de identidad, es imperativo realizar dos acciones previas

  • Cree un tipo de usuario que se derive de IdentityUser
  • Actualice nuestro para que se derive de AppDbContextIdentityDbContext<>

El primer punto no es estrictamente necesario, ya que puedes utilizar IdentityUser directamente en tus aplicaciones si es preciso. No obstante, dado que es probable que desees personalizar tu tipo de usuario en algún momento, considero que tiene sentido optar por un tipo personalizado más pronto que tarde.

public class AppUser : IdentityUser
{
    // Add customisations here later
}

                            //  Change from DbContext to IdentityDbContext<>
public class AppDbContext : IdentityDbContext<AppUser>
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }
}

Con la existencia de estos tipos en la aplicación, se abre la oportunidad de agregar los servicios de identidad mediante el recién incorporado método AddIdentityApiEndpoints<> y configurar los almacenes estándar de Identity en EF Core:

builder.Services
    .AddIdentityApiEndpoints<AppUser>()
    .AddEntityFrameworkStores<AppDbContext>();

El método AddIdentityApiEndpoints<> hace varias cosas:

  • Configura la autenticación a través de tokens y cookies (se proporcionará mayor información sobre la autenticación de cookies en publicaciones posteriores). 
  • Agrega los servicios clave de identidad, como el UserManager. 
  • También, incluye los servicios necesarios para los puntos de conexión de la API de identidad, como los proveedores de tokens SignInManager y una implementación sin operaciones de IEmailSender.

Una vez que hemos integrado todos estos servicios, finalmente podemos ejecutar algunas migraciones y establecer la base de datos.

Creación de la base de datos

Dado que la aplicación se vale de EF Core, resulta necesario generar una migración y actualizar la base de datos. Si la compilación de la aplicación se realiza sin inconvenientes, deberías ser capaz de llevar a cabo ambos pasos con el siguiente comando:

dotnet ef migrations add InitialSchema
dotnet ef database update

Si todo procede según lo esperado, esto debería conllevar la creación del archivo de base de datos SQLite llamado my_test_app.db. Estamos a punto de poner a prueba nuestra aplicación.

Agregar autorización a una API

En última instancia, nuestro objetivo es asegurar nuestras API mediante la autorización. Para garantizar un funcionamiento correcto, añadamos un requisito de autorización al punto de acceso correspondiente al pronóstico del tiempo:

app.MapGet("/weatherforecast", () => /* not show for brevity */)
  .WithName("GetWeatherForecast")
  .RequireAuthorization() // 👈 Add this
  .WithOpenApi();

En caso de ejecutar la aplicación y tratar de llegar a este punto de conexión, te enfrentarás a un error que señalará la ausencia del middleware de autorización en la aplicación.

System.InvalidOperationException: Endpoint HTTP: GET /weatherforecast contains
authorization metadata, but a middleware was not found that supports authorization.
Configure your application startup by adding app.UseAuthorization() in the application
startup code. If there are calls to app.UseRouting() and app.UseEndpoints(...), 
the call to app.UseAuthorization() must go between them.

Por lo tanto, para abordar esta situación, incluye los servicios de autorización y agrega el middleware de autorización en WebApplicationBuilder. En este punto, la aplicación debería mostrar una apariencia similar a la siguiente:

var builder = WebApplication.CreateBuilder(args);

// Add the authorization services
builder.Services.AddAuthorization();

// Add EF Core
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"))); 

// add identity services
builder.Services
    .AddIdentityApiEndpoints<AppUser>()
    .AddEntityFrameworkStores<AppDbContext>();

// Swagger/OpenAPI services
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization(); // 👈 Add Authorization middleware

// ...

Agregar puntos de conexión de la API de identidad

Para incluir los puntos de conexión de identidad en la aplicación, invoca MapIdentityApi dentro de MapIdentityApi. Esto introduce una amplia variedad de puntos de acceso en la aplicación (que se detallarán en breve).

Es probable que desees restringirlos a una subruta como /account o /identity, de manera que en lugar de los puntos de conexión /login y /confirmEmail, tendrás /account/login y /account/confirmEmail, por ejemplo. Puedes añadir el prefijo mediante MapGroup, como se ejemplifica en el siguiente caso:

app.MapGroup("/account").MapIdentityApi<AppUser>();

Con esto, ¡hemos completado el proceso! Ahora es el momento de sumergirse en la aplicación y descubrir qué capacidades tiene para ofrecer.

API de identidad

La forma más cómoda de visualizar los puntos de conexión disponibles es ejecutar la aplicación y dirigirse a /swagger/index.html para examinar la documentación de SwaggerUI:

En la parte superior de la lista, se encuentra la API /weatherforecast, y debajo de ella están todos los puntos finales agregados por MapIdentityApi<>(). Si bien es posible utilizar SwaggerUI para interactuar con la API, opté por no perder tiempo con tokens en la interfaz de usuario, y en su lugar, decidí utilizar el soporte integrado de Rider para HttpClient.

Prueba del punto de conexión protegido

El detalle inicial que atrajo mi atención fue que la ventana de Puntos finales de Rider detectaba el punto final /weatherforecast, pero no identificaba los puntos finales de identidad, incluso con la opción Mostrar desde bibliotecas habilitada. A pesar de esto, es el primer punto de acceso que deseamos verificar para asegurarnos de su seguridad. Al hacer clic derecho en el punto de acceso, opté por Generar solicitud en el cliente HTTP:

Esto generó una simple solicitud ‘GET» al punto de conexión en un archivo .http:

Al hacer clic en el ícono de Ejecutar en el medio, se lanza la solicitud al punto de acceso y, como era de anticipar, se recibe una respuesta Unauthorized, indicando que la API estaba protegida correctamente:

GET http://localhost:5117/weatherforecast

HTTP/1.1 401 Unauthorized
Content-Length: 0
Date: Sat, 02 Sep 2023 19:46:02 GMT
Server: Kestrel
WWW-Authenticate: Bearer

El primer paso para llamar a este punto de conexión es crear un usuario con el que podamos iniciar sesión.

Registrar usuario nuevo

El primer punto de conexión que exploraremos es el punto de acceso /register (expuesto en mi ejemplo como /account/register). Se trata de un punto final POST al que enviamos una dirección de correo electrónico, un nombre de usuario y una contraseña:

### Register a new user
POST http://localhost:5117/account/register
Content-Type: application/json

{
  "username": "andrew@example.com",
  "password": "SuperSecret1!",
  "email": "andrew@example.com"
}

Si esta acción tiene éxito, recibirás una respuesta 200. Ten en cuenta que la contraseña que envíes debe cumplir con todos los requisitos estándar de IdentityOptions; de lo contrario, será rechazada si no satisface los criterios de longitud o complejidad, por ejemplo.

Recuperación de un token de acceso

Si el usuario ha sido creado exitosamente, el paso siguiente implica obtener un token de acceso. Podemos utilizar la ruta /login para llevar a cabo esta operación.

### Login and retrieve tokens
POST http://localhost:5117/account/login
Content-Type: application/json

{
  "username": "andrew@example.com",
  "password": "SuperSecret1!"
}

Si proporciona un nombre de usuario y una contraseña válidos, esto devuelve una respuesta similar a la que vio anteriormente:

{
  "token_type": "Bearer",
  "access_token": "CfDJ8CuDyfVIT-VKm_2z2YS9T0jen4IyKKwsovVDRrrFyC_nU4HRXb...",
  "expires_in": 3600,
  "refresh_token": "CfDJ8CuDyfVIT-VKm_2z2YS9T0gvL1EYfbVBnppccNrI6WrfRcsOb..."
}

Podemos usar el scripting integrado de Rider HttpClient para tomar automáticamente este token de la respuesta y guardarlo en una variable que podemos usar más tarde:

> {% 
    client.global.set("access_token", response.body.access_token); 
    client.global.set("refresh_token", response.body.refresh_token); 
%}

Así que todo se ve así en Rider:

Llamar a la API protegida

Ahora que contamos con un access_token, podemos emplearlo para realizar solicitudes a la API protegida:

### Call Forecast API with bearer token
GET http://localhost:5117/weatherforecast
Authorization: Bearer {{access_token}}

Json devuelto:

[
  {
    "date": "2023-09-03",
    "temperatureC": 20,
    "summary": "Chilly",
    "temperatureF": 67
  },
  ...
]

Generación de un token de actualización

En el transcurso del tiempo, el token de acceso expirará, lo que nos llevará a buscar uno nuevo. Esta tarea se puede llevar a cabo utilizando el endpoint /refresh.

### Fetch a new access token
POST http://localhost:5117/account/refresh
Content-Type: application/json

{
  "refreshToken": "{{refresh_token}}"
}

Hay una variedad de otros puntos de acceso a los que puedes acceder para administrar tokens 2FA para el usuario, entre otras cosas, pero dado que esta publicación ya es lo suficientemente larga.

Conclusiones

En resumen, en esta publicación se ha explorado exhaustivamente el estado actual de ASP.NET Identity y las diversas abstracciones que ofrece, destacando la funcionalidad proporcionada por la interfaz de usuario predeterminada de Razor Pages. Sin embargo, se han señalado las limitaciones y complejidades asociadas con el estilo y la integración con SPA o aplicaciones móviles.

Posteriormente, se proporcionó una guía detallada sobre la incorporación de ASP.NET Identity a una nueva aplicación, abordando la configuración desde una plantilla en blanco e incluyendo EF Core, Identity y las API correspondientes. Se demostró cómo interactuar con estas APIs utilizando un cliente HTTP. Estas reflexiones ofrecen una visión completa de ASP.NET Identity y sirven como guía para su implementación efectiva en aplicaciones nuevas o existentes.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *