Integrate Your .NET Core API With FusionAuth
Integrate Your .NET Core API With FusionAuth
In this article, you are going to learn how to integrate a .NET Core API with FusionAuth. This presupposes you've built an application that is going to retrieve an access token from FusionAuth via one of the OAuth grants. The grant will typically be the Authorization Code grant for users or the Client Credentials grant for programmatic access.
The token provided by FusionAuth can be stored by the client in a number of locations. For server side applications, it can be stored in a database or on the file system. In mobile applications, store them securely as files accessible only to your app. For a browser application like a SPA, use a cookie if possible and server-side sessions if not.
Here’s a typical API request flow before integrating FusionAuth with your .NET Core API.
Here’s the same API request flow when FusionAuth is introduced.
This document will walk through the use case where a .NET Core API validates the token. You can also use an API gateway to verify claims and signatures. For more information on doing that with FusionAuth, visit the API gateway documentation.
FusionAuth has a Hosted Backend APIs feature that makes it easier to integrate your API with FusionAuth. These APIs provide a prebuilt solution for getting your app up and running using the OAuth2 Authorization Code grant with PKCE. We have in the past shown you how to create these endpoints yourself, but this solution allows you to get going with your app without writing any backend code dealing with OAuth2. The Hosted Backend APIs deal with the OAuth2 flow and store the client tokens in cookies for you. Your service API can then check the cookies to verify the user is authenticated and authorized. For this to work, your FusionAuth instance, frontend, and API must all be hosted on the same domain.
Prerequisites
For this tutorial, you’ll need to have .NET Core, ASP.NET, npx installed.
You'll also need Docker, since that is how you’ll install FusionAuth.
The commands below are for macOS, but are limited to mkdir
and cd
, which have equivalents in Windows and Linux.
Although this guide shows how to build the .NET Core application using command-line tools, you can also use Visual Studio to build and debug the project.
Download and Install FusionAuth
First, make a project directory:
mkdir integrate-fusionauth && cd integrate-fusionauth
Then, install FusionAuth:
curl -o docker-compose.yml https://raw.githubusercontent.com/FusionAuth/fusionauth-containers/master/docker/fusionauth/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/FusionAuth/fusionauth-containers/master/docker/fusionauth/.env
docker compose up -d
Create a User and an API Key
Next, log into your FusionAuth instance. You’ll need to set up a user and a password, as well as accept the terms and conditions.
Then, you’re at the FusionAuth admin UI. This lets you configure FusionAuth manually. But for this tutorial, you're going to create an API key and then you’ll configure FusionAuth using our client library.
Navigate to + button to add a new API Key. Copy the value of the Key field and then save the key.
It might be a value like CY1EUq2oAQrCgE7azl3A2xwG-OEwGPqLryDRBCoz-13IqyFYMn1_Udjt
.
Doing so creates an API key that can be used for any FusionAuth API call. Save that key value off as you’ll be using it later.
Configure FusionAuth
Next, you need to set up FusionAuth. This can be done in different ways, but we’re going to use the .NET Core client library. You can use the client library with an IDE of your preference as well.
First, make a directory:
mkdir setup-fusionauth && cd setup-fusionauth
Then, create a .NET Core project:
dotnet new console --output SetupFusionauth && cd SetupFusionauth
Next, you’ll need to import a few NuGet packages:
dotnet add package JSON.Net
dotnet add package FusionAuth.Client
Copy and paste the following code into Program.cs
.
using System;
using io.fusionauth;
using io.fusionauth.domain;
using io.fusionauth.domain.api;
using io.fusionauth.domain.api.user;
using System.Collections.Generic;
using Newtonsoft.Json;
using io.fusionauth.domain.oauth2;
using io.fusionauth.domain.search;
namespace Setup
{
class Program
{
private static readonly String applicationName = ".NET FusionAuth Application";
private static readonly String apiKey = Environment.GetEnvironmentVariable("fusionauth_api_key");
private static readonly String fusionauthURL = "http://localhost:9011";
private static readonly String authorizedRedirectURL = "https://localhost:5001/callback";
private static readonly String logoutURL = "https://localhost:5001/";
private static readonly String applicationId = "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e";
private static readonly String clientSecret = "change-this-in-production-to-be-a-real-secret";
static void Main(string[] args)
{
if (String.IsNullOrEmpty(apiKey))
{
throw new ArgumentException("apiKey", "The API key must be set in the environment variable `fusionauth_api_key`");
}
FusionAuthSyncClient client = new FusionAuthSyncClient(apiKey, fusionauthURL);
//set the issuer up correctly
ClientResponse<TenantResponse> retrieveTenantsResponse = client.RetrieveTenants();
if (!retrieveTenantsResponse.WasSuccessful())
{
throw new Exception("couldn't find tenant");
}
//should be only one
Tenant tenant = retrieveTenantsResponse.successResponse.tenants[0];
Dictionary<String, Object> issuerUpdateMap = new Dictionary<String, Object>();
Dictionary<String, Object> tenantMap = new Dictionary<String, Object>();
tenantMap["issuer"] = fusionauthURL;
issuerUpdateMap["tenant"] = tenantMap;
ClientResponse<TenantResponse> patchTenantResponse = client.PatchTenant(tenant.id, issuerUpdateMap);
if (!patchTenantResponse.WasSuccessful())
{
throw new Exception("couldn't update tenant");
}
// generate RSA keypair
System.Guid rsaKeyId = System.Guid.Parse("356a6624-b33c-471a-b707-48bbfcfbc593");
Key rsaKey = new Key();
rsaKey.algorithm = KeyAlgorithm.RS256;
rsaKey.name = ".NET FusionAuth Application";
rsaKey.length = 2048;
KeyRequest keyRequest = new KeyRequest();
keyRequest.key = rsaKey;
ClientResponse<KeyResponse> keyResponse = client.GenerateKey(rsaKeyId, keyRequest);
if (!keyResponse.WasSuccessful())
{
throw new Exception("couldn't create RSA key");
}
// create application
Application application = new Application();
application.oauthConfiguration = new OAuth2Configuration();
application.oauthConfiguration.authorizedRedirectURLs = new List<string>();
application.oauthConfiguration.authorizedRedirectURLs.Add(authorizedRedirectURL);
application.oauthConfiguration.requireRegistration = true;
application.oauthConfiguration.enabledGrants = new List<GrantType>
{ GrantType.authorization_code, GrantType.refresh_token };
application.oauthConfiguration.logoutURL = logoutURL;
application.oauthConfiguration.proofKeyForCodeExchangePolicy = ProofKeyForCodeExchangePolicy.Required;
application.name = applicationName;
// assign key from above to sign our tokens. This needs to be asymmetric
application.jwtConfiguration = new JWTConfiguration();
application.jwtConfiguration.enabled = true;
application.jwtConfiguration.accessTokenKeyId = rsaKeyId;
application.jwtConfiguration.idTokenKeyId = rsaKeyId;
Guid clientId = Guid.Parse(applicationId);
application.oauthConfiguration.clientSecret = clientSecret;
ApplicationRequest applicationRequest = new ApplicationRequest();
applicationRequest.application = application;
ClientResponse<ApplicationResponse> applicationResponse =
client.CreateApplication(clientId, applicationRequest);
if (!applicationResponse.WasSuccessful())
{
throw new Exception("couldn't create application");
}
// register user, there should be only one, so grab the first
SearchRequest searchRequest = new SearchRequest();
UserSearchCriteria userSearchCriteria = new UserSearchCriteria();
userSearchCriteria.queryString = "*";
searchRequest.search = userSearchCriteria;
ClientResponse<SearchResponse> userSearchResponse = client.SearchUsersByQuery(searchRequest);
if (!userSearchResponse.WasSuccessful())
{
throw new Exception("couldn't find users");
}
User myUser = userSearchResponse.successResponse.users[0];
// patch the user to make sure they have a full name, otherwise OIDC has issues
Dictionary<String, Object> fullNameUpdateMap = new Dictionary<String, Object>();
Dictionary<String, Object> userMap = new Dictionary<String, Object>();
userMap["fullName"] = myUser.firstName + " " + myUser.lastName;
fullNameUpdateMap["user"] = userMap;
ClientResponse<UserResponse> patchUserResponse = client.PatchUser(myUser.id, fullNameUpdateMap);
if (!patchUserResponse.WasSuccessful())
{
throw new Exception("couldn't update user");
}
// now register the user
UserRegistration registration = new UserRegistration();
registration.applicationId = clientId;
// otherwise we try to create the user as well as add the registration
User nullBecauseWeHaveExistingUser = null;
RegistrationRequest registrationRequest = new RegistrationRequest();
registrationRequest.user = nullBecauseWeHaveExistingUser;
registrationRequest.registration = registration;
ClientResponse<RegistrationResponse> registrationResponse = client.Register(myUser.id, registrationRequest);
if (!registrationResponse.WasSuccessful())
{
throw new Exception("couldn't register user");
}
}
}
}
Update the application.oauthConfiguration.authorizedRedirectURLs
value to http://localhost:3000/profile.html
. Update the application.oauthConfiguration.logoutURL
value to http://localhost:3000
.
Now you can publish and run the application. Replace <YOUR_API_KEY>
with the API key that you generated.
The setup script is designed to run on a newly installed FusionAuth instance with only one user and no tenants other than Default
. To follow this guide on a FusionAuth instance that does not meet these criteria, you may need to modify the above script.
Refer to the .NET Core client library documentation for more information.
The path to the SetupFusionauth executable will be different depending on your platform.
dotnet publish -r osx-x64
fusionauth_api_key=<YOUR_API_KEY> bin/Debug/net7.0/osx-x64/publish/SetupFusionauth
If you are using PowerShell, you will need to set the environment variable in a separate command before executing the script.
$env:fusionauth_api_key='<your API key>'
bin\Debug\net7.0\win-x64\publish\SetupFusionauth.exe
If you want to, you can log in to your instance and examine the new API configuration the script created for you. Navigate to the tab to do so.
Create Your .NET Core API
Now you are going to create a .NET Core API. While this section builds a simple .NET Core API, you can use the same configuration to build a more complex .NET Core API.
First, create the skeleton of the .NET Core API. The .NET framework has a generator to build this out.
dotnet new webapi -o MyAPI && cd MyAPI
Then, you’ll need to import a few NuGet packages:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt
You can now start writing the code for your .NET Core API. First, let's create a controller that gives back a JSON message. Create a file called MessageController.cs
in the Controllers
folder, and add the following code to it.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace MyAPI.Controllers;
[ApiController]
[Route("message")]
[Authorize]
public class MessageController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok(new { message= "Hello"});
}
}
This controller returns a JSON object with a simple "Hello" message.
Next, update the Program.cs
file to look like this:
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 =>
{
// Get the token from a cookie
context.Token = context.Request.Cookies["app.idt"];
return Task.CompletedTask;
}
};
options.Authority = "http://localhost:9011";
options.TokenValidationParameters.ValidateAudience = false;
options.RequireHttpsMetadata = false; // For testing only, set true in production.
});
builder.Services.AddAuthorization(options =>
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
//policy.RequireClaim("scope", "api1");
})
);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: "AllowLocalFrontendCORS",
policy =>
{
policy
.WithOrigins("http://localhost:3000")
.AllowCredentials();
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors("AllowLocalFrontendCORS");
//app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Note the changes in the builder.Services.AddAuthentication()
section. This updates the authentication code to extract the JWT from the app.idt
cookie. The app.idt
cookie has an OpenID Connect JWT that contains the user's identity and claims. The app.idt
cookie is set by the Hosted Backend APIs when the user logs in. The app.idt
cookie is set to the same domain as the FusionAuth instance. This allows the .NET Core API to read the cookie and extract the JWT. Also note that the issuer is set to http://localhost:9011
which is the default FusionAuth URL. If you are using a different URL, you should update this value. The .NET authentication libraries will seamlessly validate the JWT, including the audience, and extract the identity and claims. There is no more you need to do on your part!
You can now start up your server. You should do it in a new terminal window so that you can continue to edit the .NET Core code.
To see the results, publish this application and run it. There are multiple ways of deploying an application, but publishing ensures your deployment process is repeatable. You can use the RID catalog to build different versions of this application for different platforms. Here’s the command to publish a standalone executable you could deploy behind a proxy like Nginx:
dotnet publish -r osx-x64
Then run the executable:
ASPNETCORE_ENVIRONMENT=Development bin/Debug/net7.0/osx-x64/publish/MyAPI
You can use curl to see the output of the API:
curl -v http://localhost:5000/message
This command should return a 401 Unauthorized
response, indicating that the route is secure. The next step is to set up the FusionAuth Hosted Backend API and log in to get a JWT in a cookie that your API can read.
Create a Login Redirect Page
To demonstrate how simple it is to use the Hosted Backend API, you are going to create a simple login page that redirects to the Hosted Backend API. The login page will be a plain HTML page with no special code or libraries. This page will manage the redirect to the FusionAuth Hosted Backend API login, which will handle the OAuth flow, and then have a button to call your API with the JWT cookie.
Create a new directory for the login page:
mkdir LoginPage && cd LoginPage
Then, create two new files: index.html
and profile.html
. Add the following code to the index.html
file:
<html>
<head>
<title>FusionAuth Hosted Backend API Demo</title>
</head>
<body>
<h1>FusionAuth Hosted Backend API Demo</h1>
<p>
<a href="http://localhost:9011/app/login/e9fdb985-9173-4e01-9d73-ac2d60d1dc8e?redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fprofile.html&state=1234&scope=openid%20offline_access">Login</a>
</p>
</body>
</html>
This page is the main page, which has a link that redirects to the FusionAuth Hosted Backend API login route. Note the format of the URL in the anchor href
: GET /app/login/{clientId}?redirect_uri={redirectUri}&state={state}&scope={scope}
.
In this case, clientId
is e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
, as this is hardcoded in the setup script.
The redirect_uri
is the URL the Hosted Backend API will redirect to when the login flow is complete, in this case, the profile.html
page.
The state
parameter is any information that you would like returned on successful login.
The scope
is the OAuth scopes you want to request. In this example, the openid
scope is requested, which is required to get the app.idt
cookie containing the JWT with the user's claims. You can request other scopes too, such as offline_access
to get a refresh token, which can be used to get a new access token when the current one expires. The refresh token is stored in a cookie called app.rt
.
Now add the following to the profile.html
file:
<html>
<head>
<title>FusionAuth Hosted Backend API Demo</title>
</head>
<body>
<h2>Here is your FusionAuth Profile, from calling the /me endpoint on FusionAuth</h1>
<p><em> if it is not showing, it means you are not logged in</em></p>
<pre id="profile"></pre>
<button id="call-api-button">Call the API</button>
<h2>Here are the results of calling the API</h2>
<pre id="api-results"> </pre>
</body>
<script>
(async () => {
const response = await fetch(
"http://localhost:9011/app/me/",
{
method: 'GET',
credentials: "include"
}
);
const fusionProfile = await response.json();
document.getElementById("profile").innerHTML = JSON.stringify(fusionProfile, null, 2);
}
)();
// Call the API on button press
document.getElementById("call-api-button").onclick = async function () {
let response = await fetch(
"http://localhost:5000/message",
{
method: 'GET',
credentials: "include"
}
);
if (response.status == 401) {
// Refresh the token
console.log("401 error, refreshing token...");
const refreshResponse = await fetch(
"http://localhost:9011/app/refresh/e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
{
method: 'POST',
credentials: "include",
body: ""
}
);
// retry the call to the api after refresh:
console.log("Retrying the call to the API..")
response = await fetch(
"http://localhost:5000/message",
{
method: 'GET',
credentials: "include"
}
);
}
const message = await response.json();
document.getElementById("api-results").innerHTML = JSON.stringify(message, null, 4);
}
</script>
</html>
This page is the page that the Hosted Backend API will redirect to after the login flow is complete.
The page first calls the /me
route, which is part of the FusionAuth Hosted Backend API and returns the identity of the currently logged-in user.
The profile.html
page also has a button that calls the message
route of the .NET Core API. Note that the fetch
call for this button includes the credentials: 'include'
option. This option tells the browser to include the cookies in the request so that the .NET Core API can read the app.idt
cookie and extract the JWT.
If the API call is successful, the message
route will return a "Hello" message. If the API call is not successful, the message
route will return a 401 Unauthorized
response. In this case, the code will try refreshing the access token using the refresh token in the app.rt
cookie. To do this, the code makes a fetch
request to the refresh token route of the Hosted Backend API. If the refresh token is valid, FusionAuth's Hosted Backend API will return a new access token. The code then tries the message
route again with the new access token.
A note on CORS (Cross-Origin Resource Sharing): For this setup to work, all components, including the web page, the .NET Core API, and FusionAuth, must be on the same domain. This is because the app.idt
JWT cookie and app.rt
refresh cookie are set to the same domain as the FusionAuth instance. Since everything runs on the same domain, CORS won't usually be an issue. However, you'll be running all of this on localhost
to test, with each component running on a different port, which will cause CORS issues. For this reason, we need to enable CORS on FusionAuth for the login webpage to access the FusionAuth Hosted Backend API. To do this, navigate to on the sidebar, and then select the tab. Enable CORS, and check GET, POST, and OPTIONS. Enable Allow Credentials, and add http://localhost:3000
to the list of Allowed origins.
You can now serve up the login and profile static pages. An easy way to do this is to use the http-server package via npx
. You can use any other web server to serve the files, but accessing them directly from the filesystem won't work. Change to the LoginPage
directory and run the following command:
npx http-server -a localhost -p 3000
This will serve the website on http://localhost:3000
Testing the API Flow
Navigate to the login webpage at http://localhost:3000
. Click the Login button. This will redirect you to the FusionAuth Hosted Backend API login page. Enter the email and password of the user created in the setup script earlier for your FusionAuth instance. You will be redirected to the login webpage, and the results from the /me
FusionAuth endpoint alongside the Call the API button will be shown. Click this button to call the message
route of the .NET Core API, which will echo back a "Hello" message. You should see something similar to the following output:
Feedback
How helpful was this page?
See a problem?
File an issue in our docs repo
Have a question or comment to share?
Visit the FusionAuth community forum.