Skip to content
Bruno Sonnino
Menu
  • Home
  • About
Menu

Implementing Authentication in Minimal API endpoints

Posted on 12 August 2023

Some time ago, I wrote this article about creating a Minimal API CRUD using Entity Framework. That project worked fine, but something was missing: Authentication and Authorization. When dealing with an API that can access sensitive data, it's crucial to restrict access to ensure that only authorized users can access it.

We will take on that project and add authentication and authorization to its endpoints, to secure them.

Authentication and Authorization

First, let's differentiate these two concepts:

Authentication is the process of verifying the identity of a user. The primary goal of authentication is to answer the question, "Who are you?". When the user enters its identification with the use of some kind of credentials, such as usernames, passwords, or more advanced techniques like biometric data (fingerprint, facial recognition, etc.), security tokens, or smart cards, the service compares it with the stored values to validate the user's identity and give access to the system.

Once authenticated, the system must verify if the user is allowed to use the resources and at what level. In other words, the primary goal of authorization is to answer the question, "What are you allowed to do?"
Authorization is based on the user's role, permissions, and privileges within the system. Permissions are defined in access control policies and can be specific to individual users or user groups. For example, some users may have read-only access, while others may have read and write access, and some may have administrative privileges.

We will add both authentication and authorization to our project to safeguard the data.

Minimal APIs support these authentication strategies:

  • JWT bearer-based
  • OpenID Connection-based

In this article, we will focus on implementing token-based authentication using JWT (JSON Web Tokens). JWTs are a popular choice due to their simplicity, compactness, and ease of use.

Enabling authentication in the project

We will work with the project at https://github.com/bsonnino/CustomerService. Clone the project in your local disk, run it, and open a browser page with the address https://localhost:7191/swagger/index.html (the port number may change). You'll be greeted by the Swagger test page, which lets you test the APIs and see that they're not restricted.

To enable authentication in the project, add the package Microsoft.AspNetCore.Authentication.JwtBearer using the following command:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Dos

and then register the authentication and authorization middlewares with

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<CustomerDbContext>();
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();
app.UseAuthentication();
app.UseAuthorization();
C#

Running the program at this stage won't yield any visible changes since we haven't yet secured our endpoints:

  • Use the [Authorize] attribute on the method for the endpoint
  • Add the RequireAuthorization to the endpoint

The first method will be used for the customers endpoint, and the second method will be applied to the customers/{id} endpoint:

app.MapGet("/customers", [Authorize]async (CustomerDbContext context) =>
{
    logger.LogInformation("Getting customers...");
    var customers = await context.Customer.ToListAsync();
    logger.LogInformation("Retrieved {Count} customers", customers.Count);
    return Results.Ok(customers);
});

app.MapGet("/customers/{id}", async (string id, CustomerDbContext context) =>
{
    var customer = await context.Customer.FindAsync(id);
    if (customer == null)
    {
        return Results.NotFound();
    }

    return Results.Ok(customer);
}).RequireAuthorization();
C#

Now, attempting to use any of the endpoints without proper authentication will result in a 401 response (unauthorized):

To authenticate, the call must include a token issued by a central authority, such as an identity server, which we don't currently possess. During development, the dotnet user-jwts tool can be used, invoked with the following command:

dotnet user-jwts create
Dos

Executing this command generates a token and stores it in the user secrets. It will also modify the appsettings.Development.json file to enable the toke issuer (ensure this file is present in the project folder to avoid errors):

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Authentication": {
    "Schemes": {
      "Bearer": {
        "ValidAudiences": [
          "http://localhost:30888",
          "https://localhost:44321",
          "https://localhost:7191",
          "http://localhost:5057"
        ],
        "ValidIssuer": "dotnet-user-jwts"
      }
    }
  }
}
XML

By visiting https://jwt.io, you can input the token and view it in decoded form:

At this point, you can run the project and use the following curl command to query the service:

curl -X GET https://localhost:7191/customers -H "accept: */*" -H "Authorization: Bearer <token>"
Dos

While effective, this approach can be cumbersome for API testing. Using Swagger for testing would be more convenient. However, upon opening the Swagger page, you'll notice there's no provision for entering the token. This can be easily resolved by adjusting the configuration options for Swagger in the Program.cs file:

builder.Services.AddSwaggerGen(options => {
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() {
        Name = "Authorization",
            Type = SecuritySchemeType.ApiKey,
            Scheme = "Bearer",
            BearerFormat = "JWT",
            In = ParameterLocation.Header,
            Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 1safsfsdfdfd\"",
    });
    options.AddSecurityRequirement(new OpenApiSecurityRequirement {
        {
            new OpenApiSecurityScheme {
                Reference = new OpenApiReference {
                    Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                }
            },
            new string[] {}
        }
    });
});
C#

AddSecurityDefinition instructs Swagger to incorporate the authorization feature. This will add an Authorize button at the top and configure the authorization. This configuration specifies the use of the Bearer scheme in the header, using JWT format.

AddSecurityRequirement defines the security prerequisites for your API endpoints. In this case, we are telling that Swagger needs to add the Bearer scheme to each call. When you run the program again, you can see the Authorize button. Clicking on it, you can add your token:

Subsequently, the token will be automatically added to the headers, preventing any further 401 errors:

Enabling authorization

Up to this point, we've implemented authentication (verifying the user's claimed identity), but we haven't addressed authorization. For instance, while any authenticated user can access the customer list or view an individual customer, only Admin users should be able to modify data. To achieve this, we can modify the RequireAuthorization method by adding specific parameters:

app.MapPost("/customers", async (Customer customer, CustomerDbContext context) =>
{
    context.Customer.Add(customer);
    await context.SaveChangesAsync();
    return Results.Created($"/customers/{customer.Id}", customer);
}).RequireAuthorization(new AuthorizeAttribute() { Roles = "Admin" });
C#

However, if you run the program now—despite having the previous token—the attempt to add a new customer will fail, as the token lacks the Admin role:

To address this, we need to create a new token with the Admin role:

Using this token, we now can create a new user:

More complex authorization requirements

Sometimes, more elaborated policies are necessary, which demand more than just the role. For example, we might stipulate that only a token with the can_delete_user claim is authorized to delete a customer. For that, we have to add a new policy in the AddAuthorization method:

builder.Services.AddAuthorization(options => {
    options.AddPolicy("DeleteUser", policy => policy.RequireClaim("can_delete_user", "true"));
});
C#

And include this requirement within the MapDelete call:

app.MapDelete("/customers/{id}", async (string id, CustomerDbContext context) =>
{
    var currentCustomer = await context.Customer.FindAsync(id);

    if (currentCustomer == null)
    {
        return Results.NotFound();
    }

    context.Customer.Remove(currentCustomer);
    await context.SaveChangesAsync();
    return Results.NoContent();
}).RequireAuthorization(new AuthorizeAttribute() { Policy = "DeleteUser" });
C#

As a result, we're unable to delete the user even with the Admin role:

This issue can be resolved by obtaining a new token:

Armed with this new token, we can now proceed to delete the user:

By combining policy requirements with role requirements, intricate scenarios for various operations can be established and effortlessly applied to our endpoints.

Conclusion

In summary, integrating authentication and authorization into our minimal APIs is a relatively straightforward process, and the effectiveness can be tested using the generated tokens. In a production environment, these tokens would be substituted by an Authentication Server, which identifies users and generates tokens for API usage.

All the code for this article is at https://github.com/bsonnino/CustomerAuth

1 thought on “Implementing Authentication in Minimal API endpoints”

  1. Rogier says:
    15 August 2023 at 16:36

    This is very well explained!

    Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • May 2025
  • December 2024
  • October 2024
  • August 2024
  • July 2024
  • June 2024
  • November 2023
  • October 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • June 2022
  • April 2022
  • March 2022
  • February 2022
  • January 2022
  • July 2021
  • June 2021
  • May 2021
  • April 2021
  • March 2021
  • February 2021
  • January 2021
  • December 2020
  • October 2020
  • September 2020
  • April 2020
  • March 2020
  • January 2020
  • November 2019
  • September 2019
  • August 2019
  • July 2019
  • June 2019
  • April 2019
  • March 2019
  • February 2019
  • January 2019
  • December 2018
  • November 2018
  • October 2018
  • September 2018
  • August 2018
  • July 2018
  • June 2018
  • May 2018
  • November 2017
  • October 2017
  • September 2017
  • August 2017
  • June 2017
  • May 2017
  • March 2017
  • February 2017
  • January 2017
  • December 2016
  • November 2016
  • October 2016
  • September 2016
  • August 2016
  • July 2016
  • June 2016
  • May 2016
  • April 2016
  • March 2016
  • February 2016
  • October 2015
  • August 2013
  • May 2013
  • February 2012
  • January 2012
  • April 2011
  • March 2011
  • December 2010
  • November 2009
  • June 2009
  • April 2009
  • March 2009
  • February 2009
  • January 2009
  • December 2008
  • November 2008
  • October 2008
  • July 2008
  • March 2008
  • February 2008
  • January 2008
  • December 2007
  • November 2007
  • October 2007
  • September 2007
  • August 2007
  • July 2007
  • Development
  • English
  • Português
  • Uncategorized
  • Windows

.NET AI Algorithms asp.NET Backup C# Debugging Delphi Dependency Injection Desktop Bridge Desktop icons Entity Framework JSON Linq Mef Minimal API MVVM NTFS Open Source OpenXML OzCode PowerShell Sensors Silverlight Source Code Generators sql server Surface Dial Testing Tools TypeScript UI Unit Testing UWP Visual Studio VS Code WCF WebView2 WinAppSDK Windows Windows 10 Windows Forms Windows Phone WPF XAML Zip

  • Entries RSS
  • Comments RSS
©2025 Bruno Sonnino | Design: Newspaperly WordPress Theme
Menu
  • Home
  • About