Creating an Authorization Server using OpenIdDict library


My team was recently tasked with creating an authorization server that supports OAuth2 and OIDC standards. While this has been done many times before from scratch by various people, the idea of building everything from scratch—such as the database, validation, and server layers—is not particularly appealing to me, as there are many foundational features that need to be implemented before we can get to the more exciting parts.
This is where OpenIddict (pronounced "OpenId-Addict") comes into play. It is an open-source library that provides the foundation to help you build either an Authorization Server, Client, or a Resource Server with validation using .NET Core in a very short amount of time depends on how you configure it. In this article I want to show you how quick and easy it is to get an Authorization server up and running with very little effort.
We will be using OpenIdDict 6.x with .NET 8 and for simplicity we will be using SQLite database. We will create the endpoints needed to support the Client Credential Flow.
First off, we create a new .NET 8 WebAPI project via
dotnet new webapi -n auth-openiddict --framework net8.0
Scaffolding your application to utilize OpenIdDict is very straight forward, once the project is created go into Program.cs and configure your database context.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
// Configure the context to use Microsoft SQLite server as database.
options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "AuthServer")}");
// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();
});
This will utilize Entity Framework to create the necessary schemas for OpenIddict. It will generate tables to store information such as Applications, Claims, Tokens, Scopes, Roles, etc. If you are using SQLite, a database file will be created for you after running the application for the first time. By default, this file will be located at C:\U
sers
\qphan\AppData\Local\Temp
. In this example, I named my file AuthServer
.
Next you want to register OpenIdDict which involves three portions:
OpenIddict Core components allow you to configure database integration, schema management, and how your data is stored and retrieved. Think of this as the foundation of a new house.
OpenIddict Server components enable you to configure your application to issue and manage tokens based on your requirements. This is where you can enable specific flows, define endpoints, and control their behavior. Think of this as the main functionality of the house.
OpenIddict Validation components are only required if your application is implemented as a Resource Server or a Client. These components dictate how tokens from incoming requests should be validated and how information or claims can be extracted from a token. Think of this as the security system of the house.
// Register OpenIddict core services.
builder.Services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
options.UseEntityFrameworkCore().UseDbContext<ApplicationDbContext>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
options.AddEventHandler<ProcessRequestContext>(builder =>
{
builder.Import(InferEndpointType.Descriptor)
.AddFilter<RefreshTokenConfigurationFilter>();
});
// Allow which oAuth flow in the auth server.
options.AllowClientCredentialsFlow()
.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow()
.RequireProofKeyForCodeExchange();
// Enable oAuth endpoints.
options.SetAuthorizationEndpointUris("connect/authorize")
.SetTokenEndpointUris("connect/token")
.SetIntrospectionEndpointUris("connect/introspect")
.SetRevocationEndpointUris("connect/revoke");
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableStatusCodePagesIntegration()
.DisableTransportSecurityRequirement(); //development only.
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
Next we will need to implement a controller that has the endpoints we need. When you look at the AddServer() portion of OpenIdDict, there is a line that configure the token endpoint to be connect/token
.SetTokenEndpointUris("connect/token")
Since we are mainly focus with the Client Credential Flow in this article, I will implement only the /token endpoint for a controller. It checks if the request is valid and the client is authorized then it creates a “profile” for the client using claims and generate an access token for it.
[HttpPost]
[Route("token")]
public async Task<IActionResult> Token()
{
var request = HttpContext.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("The OpenIddict server request cannot be retrieved.");
if (request.IsClientCredentialsGrantType())
{
// Note: the client credentials are automatically validated by OpenIddict:
// if client_id or client_secret are invalid, this action won't be invoked.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ??
throw new InvalidOperationException("The application cannot be found.");
// Create a new ClaimsIdentity containing the claims that
// will be used to create an id_token, a token or a code.
var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, Claims.Name, Claims.Role);
// Use the client_id as the subject identifier.
identity.SetClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application));
identity.SetClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application));
// Explicitly set destinations for claims to ensure they appear in the token
identity.SetDestinations(static claim => [Destinations.AccessToken]);
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new NotImplementedException("The specified grant is not implemented.");
}
Next we want to make sure our database is populated with OpenIdDict tables if you don’t have a database created yet.
To set up the database for the first time, navigate to the project directory and follow these steps. If you want to start fresh, remove any existing migrations by running dotnet ef migrations remove
. Next, create a new migration to define the database structure by running dotnet ef migrations add InitialCreate
. Finally, apply the migration to create or update the database by running dotnet ef database update
. This will ensure your database is ready to use with the necessary tables.
At this point, if you run the application, the endpoints should be functional. However, there won't be any client applications available to use with your requests. To address this, we include a data seeding logic that pre-populates the database with a test client. This allows us to use the Client Credentials Flow for testing purposes. The data seeding logic runs automatically when the application starts, creating the necessary client application for your requests.
public static class DataSeeding
{
public static void CreateData(IApplicationBuilder app)
{
CreateApplications(app.ApplicationServices);
}
private static async void CreateApplications(IServiceProvider serviceProvider)
{
using var scope = serviceProvider.CreateScope();
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
var credentialFlowClient = await manager.FindByClientIdAsync("private-client");
if (credentialFlowClient is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "private-client",
DisplayName = "Credential Flow Client",
ClientSecret = "credential-flow-secret",
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.Revocation,
OpenIddictConstants.Permissions.Endpoints.Introspection,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.Prefixes.Scope + "api",
}
});
}
}
}
Next, we invoke this as part of the application startup in Program.cs
DataSeeding.CreateData(app);
That’s it, this is what you need to get a bare minimum to get an authorization server up and running. In order to test this out you can issue a POST request to the /token endpoint in order to get a token:
curl --location 'http://localhost:5120/connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=private-client' \
--data-urlencode 'client_secret=credential-flow-secret'
OpenIddict, out of the box, provides a robust layer of validation through its middleware pipeline. It intercepts each client request and validates various aspects, such as whether the client ID exists, whether the client is authorized to access specific endpoints, and whether the request is permitted to use certain redirect URIs or scopes. This flexible system simplifies the development process by handling complex validation tasks, allowing developers to focus on the core logic of their applications. This topic is extensive and will be covered in detail in a future post.
Happy coding!
Subscribe to my newsletter
Read articles from Quang Phan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
