API

.NET API

.NET API

In this quickstart, you are going to learn how to integrate a .NET 7 resource server with FusionAuth. You will protect an API resource from unauthorized usage. You’ll be building it for ChangeBank, a global leader in converting dollars into coins.

The Docker Compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-dotnet-api.

Prerequisites

This app has been tested with .NET 7. This example should work with other compatible versions of .NET.

Optionally, you can also install Visual Studio to make it easier to work with .NET through an IDE.

General Architecture

A client wants access to an API resource at /resource. However, it is denied this resource until it acquires an access token from FusionAuth.

ClientResource ServerFusionAuthGET /resource404 Not AuthorizedPOST /api/login200 Ok(token)GET /resource200 Ok(resource)ClientResource ServerFusionAuth

Resource Server Authentication with FusionAuth

While the access token is acquired via the Login API above, this is for simplicity of illustration. The token can be, and typically is, acquired through one of the OAuth grants.

Getting Started

In this section, you’ll get FusionAuth up and running and create a resource server which will serve the API.

Clone The Code

First off, grab the code from the repository and change into that directory.

git clone https://github.com/FusionAuth/fusionauth-quickstart-dotnet-api.git
cd fusionauth-quickstart-dotnet-api

Run FusionAuth Via Docker

You'll find a Docker Compose file (docker-compose.yml) and an environment variables configuration file (.env) in the root directory of the repo.

Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.

docker compose up -d

Here you are using a bootstrapping feature of FusionAuth called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json file and configure FusionAuth to your specified state.

If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing docker compose down -v, then re-run docker compose up -d.

FusionAuth will be initially configured with these settings:

  • Your client Id is e9fdb985-9173-4e01-9d73-ac2d60d1dc8e.
  • Your client secret is super-secret-secret-that-should-be-regenerated-for-production.
  • Your example teller username is teller@example.com and the password is password. They will have the role of teller.
  • Your example customer username is customer@example.com and the password is password. They will have the role of customer.
  • Your admin username is admin@example.com and the password is password.
  • The base URL of FusionAuth is http://localhost:9011/.

You can log in to the FusionAuth admin UI and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.

If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app. The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the OAuth configuration section.

The .env file contains passwords. In a real application, always add this file to your .gitignore file and never commit secrets to version control.

Create Your ASP.NET Web API Resource Server

Now you are going to create an ASP.NET API application. While this section builds a simple API, you can use the same configuration to integrate an existing API with FusionAuth.

We are going to be building an API backend for a banking application called ChangeBank. This API will have two endpoints:

  • make-change: This endpoint will allow you to call GET with a total amount and receive a response indicating how many nickels and pennies are needed to make change. Valid roles are customer and teller.
  • panic: Tellers may call this endpoint to call the police in case of an incident. The only valid role is teller.

Both endpoints will be protected such that a valid JSON web token (JWT) will be required in the Authorization header or in a cookie named app.at in order to be accessed. Additionally, the JWT must have a roles claim containing the appropriate role to use the endpoint.

If you simply want to run the application, there is a completed version in the complete-application directory. You can use the following commands to get it up and running if you do not want to create your own.

cd complete-application
dotnet run

You can then follow the instructions in the Run The API section.

Now, create a base ASP.NET API project.

dotnet new webapi -o your-api && cd your-api

Add Security

This app uses the standard .NET JWTBearer package, which simplifies extracting and validating JWT tokens from FusionAuth.

Install the .NET JWTBearer package and other supporting libraries.

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer -v 7.0.14
dotnet add package Microsoft.AspNetCore.OpenApi -v 7.0.10
dotnet add package System.IdentityModel.Tokens.Jwt -v 7.0.3
dotnet add package Microsoft.IdentityModel.Protocols.OpenIdConnect -v 7.0.3

Next, replace your appsettings.json file with settings specific to the FusionAuth Issuer and Client Id (Audience).

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Urls": "http://localhost:5001",
  "Authentication": {
    "Schemes": {
      "Bearer": {
        "Authority": "http://localhost:9011",
        "ValidAudiences": [
          "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
        ]
      }
    }
  }
}

Replace the code in the appsettings.Development.json file with the following.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "Authentication": {
    "Schemes": {
      "Bearer": {
        "RequireHttpsMetadata": "false"
      }
    }
  }
}

The appsettings.Development.json file will be used when you run the application in development mode. It will enable some useful development options, mainly that HTTPS will not be required for the Issuer (FusionAuth) server. In production, you must use HTTPS for the Issuer server.

Update the Program.cs file to configure ASP.NET authentication to use JWT tokens. Replace the code in the Program.cs file with the following code.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        options.Events = new()
        {
            OnMessageReceived = context =>
            {
                // Extract the token from a cookie if available.
                context.Token = context.Request.Cookies["app.at"];
                return Task.CompletedTask;
            }
        };
    });

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

//app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

The Program.cs file configures the authentication services to use JWT bearer tokens to authenticate users through the .AddJwtBearer() method. The Issuer and Audience settings used by the JWT verification code are set in the appsettings.json file under the Authentication section. The options.Events section configures the JWT bearer authentication to use the OnMessageReceived event to read the JWT from the app.at cookie if it is present. By default, the JWT bearer authentication will only read the JWT from the Authorization header. With this code, the JWT can be passed in either the Authorization header or the app.at cookie.

The application is now configured to use FusionAuth JWTs for authentication. Next, you’ll add some routes to the application.

Write C# Code

Now you are going to write some C# code. You are going to write two API controllers and their corresponding model objects.

Add Model Objects

We will need response objects for the API to return. One is the PanicResponse, which returns a message when a successful POST call is made to /panic.

Create a Models folder.

mkdir Models

In the Models folder, create a new class PanicResponse.cs.

touch Models/PanicResponse.cs

Add the following code to it.

namespace Models
{
    public class PanicResponse
    {
        public PanicResponse(string message)
        {
            Message = message;
        }

        public string Message { get; set; }
    }
}

Next, you need an object to show the breakdown from making change. This object will hold a double total and Integers nickels and pennies. In the Models folder, create a new class Change.cs.

touch Models/Change.cs

Add the following code to it.

namespace Models
{
    public class Change
    {
        public double Total { get; set; }

        public int Nickels { get; set; }

        public int Pennies { get; set; }

        public Change()
        {
        }
    }
}

Add Controllers

Now you need to add controllers to handle API calls. In the Controllers folder, add a new class MakeChangeController.cs.

touch Controllers/MakeChangeController.cs

Add the following code to it.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Models;

namespace Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class MakeChangeController : ControllerBase
    {
        [HttpGet]
        [Authorize(Roles = "teller,customer")]
        public Change Get(double total)
        {
            var change = new Change();
            change.Total = total;

            change.Nickels = Convert.ToInt32(Math.Floor(total / 0.05));
            change.Pennies = Convert.ToInt32(Math.Round((total - 0.05 * change.Nickels) / 0.01));
            return change;
        }
    }
}

In this class, a total is an HTTP request query parameter that is converted to a double, and the get method divides the total into nickels and pennies and returns the response object.

Note the [Authorize("teller,customer")] attribute which adds the magic of ASP.NET role-based authorization to the route. The ASP.NET JWT Bearer package automatically reads out the roles claimed in the user token, and then adds them to the user object. For example, a decoded JWT for a teller user would look like this:

{
  "aud": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
  "exp": 1689289585,
  "iat": 1689285985,
  "iss": "http://localhost:9011",
  "sub": "00000000-0000-0000-0000-111111111111",
  "jti": "ebaa4184-2320-47dd-925b-2e18756c635f",
  "authenticationType": "PASSWORD",
  "email": "teller@example.com",
  "email_verified": true,
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
  "roles": [
    "teller"
  ],
  "auth_time": 1689285985,
  "tid": "d7d09513-a3f5-401c-9685-34ab6c552453"
}

ASP.NET reads the roles field, adds those roles to the user, and tests the user for the roles via the Authorize attribute.

Now, add a controller for tellers to call in an emergency. In the Controllers folder add a class PanicController.cs.

touch Controllers/PanicController.cs

Add the following code to it.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Models;

namespace Controllers;

[ApiController]
[Route("[controller]")]
public class PanicController : ControllerBase
{
    [HttpPost]
    [Authorize(Roles = "teller")]
    public PanicResponse Post()
    {
        return new PanicResponse("We've called the police!");
    }
}

This class listens on /panic for a POST request and returns a response indicating that the police were called. The attributes are the same as the MakeChangeController.cs route, except that this is a POST request, so it is named Post and has the [HttpPost] attribute marking it as such.

Run The API

Start the API resource server with the following command.

dotnet run

This command will build and run the project, which should be running on port 5001.

Get A Token

There are several ways to acquire a token in FusionAuth but for this example, you will use the Login API to keep things simple.

First, try the requests as the teller@example.com user. Based on the kickstart configuration this user has the teller role and should be able to call both /make-change and /panic.

Acquire an access token for teller@example by making the following request in a new terminal window.

curl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
  "loginId": "teller@example.com",
  "password": "password",
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'

Copy the token from the response, which should look like the following.

{
   "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTMwNTksImlhdCI6MTY4OTM1Mjk5OSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMTExMTExMTExMTExIiwianRpIjoiY2MzNWNiYjUtYzQzYy00OTRjLThmZjMtOGE4YWI1NTI0M2FjIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6InRlbGxlckBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhcHBsaWNhdGlvbklkIjoiZTlmZGI5ODUtOTE3My00ZTAxLTlkNzMtYWMyZDYwZDFkYzhlIiwicm9sZXMiOlsiY3VzdG9tZXIiLCJ0ZWxsZXIiXSwiYXV0aF90aW1lIjoxNjg5MzUyOTk5LCJ0aWQiOiJkN2QwOTUxMy1hM2Y1LTQwMWMtOTY4NS0zNGFiNmM1NTI0NTMifQ.WLzI9hSsCDn3ZoHKA9gaifkd6ASjT03JUmROGFZaezz9xfVbO3quJXEpUpI3poLozYxVcj2hrxKpNT9b7Sp16CUahev5tM0-4_FaYlmUEoMZBKo2JRSH8hg-qVDvnpeu8nL6FXxJII0IK4FNVwrQVFmAz99ZCf7m5xruQSziXPrfDYSU-3OZJ3SRuvD8bMopSiyRvZLx6YjWfBsvGSmMXeh_8vHG5fVkq5w1IkaDdugHnivtJIivHuCfl38kQBgw9rAqJLJoKRHHW0Ha7vHIcS6OCWWMDIIVspLyQNcLC16pL9Nss_5v9HMofow1OvQ9sUSMrbbkipjKq2peSjG7qA",
   "tokenExpirationInstant": 1689353059670,
   "user": {
      ...
   }
}

Make The Request

You can provide the token to the resource API via a cookie named app.at or in the HTTP Authorization header.

Now make a request to /make-change with a query parameter total=5.12 using either of the above methods.

This is a request using the Authorization header.

curl --location 'http://localhost:5001/makeChange?total=5.12' \
--header 'Authorization: Bearer <your_token>'

This is a request using a cookie.

curl --location 'http://localhost:5001/makeChange?total=5.12' \
--cookie 'app.at=<your_token>'

Your response should look like below.

{
  "total": 5.12,
  "nickels": 102,
  "pennies": 2
}

You were authorized, success! You can try making the request without the Authorization header or with a different string rather than a valid token, and see that you are denied access.

Next, call the /panic endpoint because you are in trouble!

Call the endpoint using the Authorization header as follows.

curl --location --request POST 'http://localhost:5001/panic' \
--header 'Authorization: Bearer <your_token>'

Call the endpoint using a cookie like this.

curl --location --request POST 'http://localhost:5001/panic' \
--cookie 'app.at=<your_token>'

This is a POST not a GET because you want all your emergency calls to be non-idempotent.

Your response should look like the following.

{
  "message": "We've called the police!"
}

Nice, help is on the way!

Now let’s try as customer@example.com, who has the role customer. Acquire a token for customer@example.com.

curl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
  "loginId": "customer@example.com",
  "password": "password",
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'

Your response should look like the following.

{
  "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTQxMjMsImlhdCI6MTY4OTM1MzUyMywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMjIyMjIyMjIyMjIyIiwianRpIjoiYjc2YWMwMGMtMDdmNi00NzkzLTgzMjgtODM4M2M3MGU4MWUzIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6ImN1c3RvbWVyQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImFwcGxpY2F0aW9uSWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJyb2xlcyI6WyJjdXN0b21lciJdLCJhdXRoX3RpbWUiOjE2ODkzNTM1MjMsInRpZCI6ImQ3ZDA5NTEzLWEzZjUtNDAxYy05Njg1LTM0YWI2YzU1MjQ1MyJ9.T1bELQ6a_ItOS0_YYpvqhIVknVMNeamcoC7BWnPjg2lgA9XpCmFA2mVnycoeuz-mSOHbp2cCoauP5opxehBR2lCn4Sz0If6PqgJgXKEpxh5pAxCPt91UyfjH8hGDqE3rDh7E4Hqn7mb-dFFwdfX7CMdKvC3dppMbXAGCZTl0LizApw5KIG9Wmt670339pSf5lzD38P9WAG5Wr7fAmVrIJPVu6yv2FoR-pMYD2lnAF63HWKknrWB-khmhr9ZKRLXKhP1UK-ThY1FSnmpp8eNblsBqCxf6WaYxYkdp5_F2e56M4sQwHzrg4P9tZGVCmMri4dShF3Ck7OGa7hel-iIPew",
  "tokenExpirationInstant": 1689354123118,
  "user": {
    ...
  }
}

Now use the token to call /make-change with a query parameter total=3.24.

Use the Authorization header as follows.

curl --location 'http://localhost:5001/makeChange?total=3.24' \
--header 'Authorization: Bearer <your_token>'

Or use a cookie like this.

curl --location 'http://localhost:5001/makeChange?total=3.24' \
--cookie 'app.at=<your_token>'

Your response should look like the following.

{
  "total": 3.24,
  "nickels": 64,
  "pennies": 4
}

So far so good. Now let’s try to call the /panic endpoint. We’ll add the -i flag to see the headers of the response.

Make the call using the Authorization header like this.

curl -i --request POST 'http://localhost:5001/panic' \
--header 'Authorization: Bearer <your_token>'

Or using a cookie like this.

curl -i --request POST 'http://localhost:5001/panic' \
--cookie 'app.at=<your_token>'

Your response should look like the following.

HTTP/1.1 403 Forbidden
Content-Length: 0
Date: Wed, 11 Oct 2023 08:29:09 GMT
Server: Kestrel

Uh oh, I guess you are not allowed to do that. Notice that it returns a 403 Forbidden rather than a 401 Unauthorized, which means it has authenticated the user but the user is forbidden to access that particular resource.

Enjoy your secured resource server!

Made it this far? Get a free t-shirt, on us!

Thank you for spending some time getting familiar with FusionAuth.

*Offer only valid in the United States and Canada, while supplies last.

fusionauth tshirt

Next Steps

This quickstart is a great way to get a proof of concept up and running quickly, but to run your API in production, there are some things you're going to want to do.

FusionAuth Integration

Security

Troubleshooting

  • I get Failed to connect to localhost port 9011: Couldn't connect to server when I call the Login API.

Ensure FusionAuth is running in the Docker container. You should be able to log in as the admin user, admin@example.com with a password of password at http://localhost:9011/admin.

  • The /panic endpoint doesn’t work when I call it.

Make sure you are making a POST call and using a token with the teller role.

  • It still doesn’t work

You can always pull down a complete running application and compare what’s different.

git clone https://github.com/FusionAuth/fusionauth-quickstart-dotnet-api.git
cd fusionauth-quickstart-dotnet-api
docker compose up -d
cd complete-application
dotnet run