other ways).
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.
A client wants access to an API resource at /resource
. However, it is denied this resource until it acquires an access token from FusionAuth.
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.
In this section, you’ll get FusionAuth up and running and create a resource server which will serve the API.
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
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:
e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
.super-secret-secret-that-should-be-regenerated-for-production
.teller@example.com
and the password is password
. They will have the role of teller
.customer@example.com
and the password is password
. They will have the role of customer
.admin@example.com
and the password is password
.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.
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
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.
Now you are going to write some C# code. You are going to write two API controllers and their corresponding 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()
{
}
}
}
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.
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.
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": {
...
}
}
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!
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.
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.
/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.
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