Utilizando un API key en nuestras APIs

En este post os voy a hablar sobre lo que es un API Key, qué aplicaciones tiene y como implementarla en nuestra aplicación dotNet.

¿Qué es un API Key?

Vamos a comenzar explicando qué es una API Key. API Key, son las siglas de API: Application Programming Interfaces, que se traduciría como interfaz de programación de aplicaciones, y Key: significa Llave, por lo tanto podríamos decir que es un identificador que sirve para la autenticación de un usuario para el uso de un servicio, es decir una clave para autenticarte cada vez que se utiliza.

Es importante mantener la API Key segura y no compartirla públicamente, ya que cualquier persona que tenga acceso a ella puede hacer uso de la API en nombre del titular de la clave. Si una API Key se compromete, es necesario revocarla y generar una nueva para garantizar la seguridad de los recursos protegidos por ésta.

¿Para qué utilizar un API Key?

Las API Keys tienen multitud de usos, por lo que a continuación voy a enumerar algunos de los que considero más comunes.

  • Monitoreo y límites de uso: Se utilizan para rastrear y controlar el uso de la API por parte de diferentes aplicaciones o usuarios. Esto permite establecer límites de uso y evitar abusos o sobrecargas de la API.
  • Facturación y seguimiento: En algunos casos, se utilizan para realizar un seguimiento del consumo de la API por parte de cada cliente o aplicación. Muy útil para propósitos de facturación en modelos de pago por uso.
  • Seguridad: Al requerir una API Key para acceder a la API, se agrega una capa adicional de seguridad, ya que solo aquellos que poseen una clave válida pueden interactuar con la API.

¿Cómo añadir API Key a mis API?

La implementación de un API Key en una API de dotNet puede variar según las necesidades y preferencias específicas de la aplicación. No obstante, a continuación muestro una forma bastante extendida de implementarla mediante el uso de un middleware personalizado.

El primer paso es crear el middleware que valide la API Key recibida en cada petición.

public class ApiKeyMiddleware
{
    private readonly IConfiguration _configuration;
    private readonly RequestDelegate _next;

    public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration) 
    {
        _next = next;
        _configuration = configuration;
    }
    
    public async Task InvokeAsync(HttpContext context) 
    {
        if (!context.Request.Headers.TryGetValue("X-API-Key", out var extractedApiKey)) 
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("Api Key was not provided ");
            return;
        }
        
        var apiKey = _configuration.GetValue<string>("X-API-Key");
        if (!apiKey.Equals(extractedApiKey)) 
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("Unauthorized client");
            return;
        }
        
        await _next(context);
    }
}

Lo único que hace el middleware anterior es recuperar la cabecera cuya clave es X-API-Key y compararla con la clave almacenada la configuración de nuestra aplicación, recuperada de las settings de proyecto.

Para que esto funcione, es necesario añadir la clave en nuestras settings.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "X-API-Key": "{YOUR API KEY}"
}

NOTA 1: Yo he utilizado X-API-Key como clave, pero se puede utilizar cualquier cadena de texto que deseemos.

NOTA 2: En el ejemplo, almaceno la clave en las settings de la aplicación, pero tendría sentido almacenarlas en una base de datos, tener varias claves para distintos propósitos, etc…

En este punto tenemos un middleware que valida el API Key, pero es necesario decirle a nuestra aplicación que lo utilice, para ello, añadimos la siguiente linea en el Program

app.UseHttpsRedirection();
app.UseAuthorization();
app.UseMiddleware<ApiKeyMiddleware>();
app.MapControllers();

app.BuildContext().Run();

Añadiendo algunos tests

Para probar que todo lo anterior, he añadido algunos tests para comprobar que la ejecución no es autorizada si no envío la clave, o envío una incorrecta. Y sí es permitida cuando la calve es correcta.

private static readonly string url = "/api/task";
private HttpClient _client;

[TestInitialize]
public async Task InitDbContext()
{
    //Creates a temporary database and inserts some data to make tests
    WebApplicationFactory<Program> factory = await BuildWebApplicationFactory(Guid.NewGuid().ToString());
    _client = factory.CreateClient();
}

[TestCleanup]
public async Task RemoveDbContext()
{
    await DeleteDatabase();
    _client.Dispose();
}

[TestMethod]
public async Task get_all_without_api_key_unauthorized()
{
    StringContent content = new StringContent(string.Empty, Encoding.UTF8, "application/json");
    HttpResponseMessage response = await _client.PostAsync($"{url}/getAll", content);

    Assert.AreEqual(System.Net.HttpStatusCode.Unauthorized, response.StatusCode);
}

[TestMethod]
public async Task get_all_wrong_api_key_unauthorized()
{
    _client.DefaultRequestHeaders.Add("X-API-Key", "wrong-key");

    StringContent content = new StringContent(string.Empty, Encoding.UTF8, "application/json");
    HttpResponseMessage response = await _client.PostAsync($"{url}/getAll", content);

    Assert.AreEqual(System.Net.HttpStatusCode.Unauthorized, response.StatusCode);
}

[TestMethod]
public async Task get_all_ok()
{
    _client.DefaultRequestHeaders.Add("X-API-Key", "correct-key");

    TaskPaginationRequest pagination = new TaskPaginationRequest { TaskListId = 1, PageSize = 2, PageNumber = 1 };
    StringContent content = new StringContent(JsonConvert.SerializeObject(pagination), Encoding.UTF8, "application/json");
    HttpResponseMessage response = await _client.PostAsync($"{url}/getAll", content);

    Assert.AreEqual(System.Net.HttpStatusCode.OK, response.StatusCode);
}

Utilizando la API Key solo en algunos endpoints

Al igual que la implementación del middleware, en este punto también tendríamos varias posibles implementaciones, todas ellas validas, pero a mi la que más me gusta es la de añadir el middleware de forma condicional.

Imaginemos que tenemos un API con dos controllers. Uno de lista de tareas (tasklist) y otro de tareas (task). Teniendo en cuenta ese escenario, se podría decidir que el middleware se lance solo si la url comienza por /api/tasklist/. De esa manera, los endpoints de tareas se podrían ejecutar sin clave y los de listas sí que la necesitarían.

app.UseHttpsRedirection();
app.UseAuthorization();
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api/tasklist/"), appBuilder => appBuilder.UseMiddleware<ApiKeyMiddleware>());
app.MapControllers();

app.BuildContext().Run();

Del mismo modo, podría decidir que solo los métodos DELETE requieren del uso de clave. En tal caso, el middleware se añadiría de la siguiente manera.

app.UseHttpsRedirection();
app.UseAuthorization();
app.UseWhen(context => context.Request.Method.Equals("DELETE"), appBuilder => appBuilder.UseMiddleware<ApiKeyMiddleware>());
app.MapControllers();

app.BuildContext().Run();

Esta es solo una de las formas de implementar un API Key en una API de dotNet. Dependiendo de las necesidades, también se pueden considerar otras opciones, como el uso de bibliotecas de autenticación, tokens JWT o integración con sistemas de autenticación y autorización más complejos.

Foto de Towfiqu barbhuiya en Unsplash

Un comentario sobre “Utilizando un API key en nuestras APIs

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.