OpenId Connect and OAuth2.0 server in AspNetCore using OpenIdDict

Siarhei Kharlap
14 min readMay 13, 2023

Before we move on, it is essential to understand the scope of the problem we will be addressing.

What does OpendIdDict allow us to reach?

OpenIdDict is an implementation of OpenId Connect(OIDC) and OAuth 2.0 specifications, that is applicable to AspNetCore applications. In this specific story, I will describe Authorization server implementation.

If you are not familiar with OpenId Connect(OIDC) and OAuth 2.0, I highly recommend you check out this resource: [OAuth]

Just in case you are busy and don’t have time to read it, I’m covering the basics here.

What is OAuth 2.0?

OAuth2 is an authorization framework that allows users to grant limited access to their resources from one site to another site without having to share their credentials. In other words, it allows a user to authenticate with one application, such as Facebook, and then use that authentication to access a separate application, such as Spotify, without having to provide their credentials to Spotify.
Commonly JWT access tokens are used as a grant for accessing protected resources.

What is OpenID Connect(OIDC)?

OpenID Connect (OIDC) is a simple identity layer on top of OAuth 2.0. It allows for the exchange of identity-related information between a client application, a user, and an identity provider. OIDC provides a standardized way to authenticate and authorize users, as well as to obtain user information such as their name and email address.
It builds on top of OAuth 2.0 by adding an ID Token that contains user identity information, and a discovery mechanism for finding the necessary endpoints to interact with the identity provider.

Roles or Who is Who?

OAuth defines four roles:

  • Resource owner (the user or application on whose behalf requests are made)
  • Resource server (the API, which should be protected)
  • Authorization server (the central trusted authority that authenticates the user, determines whether they have access to the requested resource, and issues an access token)
  • Client (the application, which needs to access the Resource Server)

Besides roles, there are other terms I would like to cover.

  • access_token — is the string (mostly in JWT format, but it isn’t required) used when making authenticated requests to the API
  • refresh_token — is a string that is used to get a new access token when an access token expires

[more…]

Self-contained Jwt Access_Token

The original OAuth 2.0 Authorization Framework [RFC6749] specification does not mandate any specific format for access tokens. but JWT has become a de facto format.
Token Data Structure [more here].

{
//identifies the resource owner
"sub": "test_client",

//identifies the recipients that the JWT is intended for
"aud": "test_resource",

//requested scopes
"scope": "test_scope",

// jti - unique identifier for the JWT
"jti": "b45fe1a4-08b2-4193-813b-376da6bc3cd9",

//exp - the expiration time on
//or after which the JWT MUST NOT be accepted for processing
"exp": 1683716709,

//iss identifies the principal that issued the JWT
"iss": "https://localhost:7153/",

//iat - the time at which the JWT was issued
"iat": 1683713109
}

One point I’d like to emphasize is the “aud” (audience) claim, which identifies the recipients that the JWT is intended for, that means that one token may be intended for more than one recipient.

1 Let's get started

To begin, create a new ASP.NET Core project or use an existing one. I recommend using .NET 6, but any higher version should also be suitable.

Update your .csproj file to reference the OpenIddict packages

<PackageReference Include="OpenIddict.AspNetCore" Version="4.2.0" />
<PackageReference Include="OpenIddict.Core" Version="4.2.0" />
<PackageReference Include="OpenIddict.Server" Version="4.2.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

I also will use some additional packages, so you may need to add them to your project as well.

   
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="4.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.15" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.15" />

I am using the following launchSettings.json file, which contains configuration settings for launching the application. This file commonly specifies environment variables, command-line arguments, and other configuration settings. In my case, I am using it to configure the application's launch profile:

  {
"profiles": {
"OpenIdDictSample.Server": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": ".well-known/openid-configuration",
"applicationUrl": "https://localhost:7153;",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}

To configure OpenIddict in your application, add the following code snippet to your Program.cs file. This code sets up the OpenIddict services in the application pipeline.:

builder.Services.AddOpenIddict()
.AddServer(
_ =>
{
//enable client_credentials grant_tupe support on server level
_.AllowClientCredentialsFlow();
//specify token endpoint uri
_.SetTokenEndpointUris("token");
//secret registration
_.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
_.DisableAccessTokenEncryption();
//the asp request handlers configuration itself
_.UseAspNetCore().EnableTokenEndpointPassthrough();
}
);

...

//!!!Ensure, that you are using the Authentication middleware
app.UseAuthentication();

If you run your application with these changes, you should see a message indicating OpenIddict has been successfully configured. This message confirms that OpenIddict services and middleware have been added to your application and are ready to use:

OIDC is designed to be a self-documented framework with a default ‘.well-known/openid-configuration’ discovery endpoint. This endpoint lets clients discover the OIDC provider’s configuration and capabilities dynamically. The configuration includes things like the provider’s endpoints, supported authentication methods, and other important details. In the previous step, we set up a default configuration that will be returned when the discovery endpoint is accessed. You can see the default configuration we made in the image above.

As we can see “token_endpoint”: “https://localhost:7153/token” which was configured by _.SetTokenEndpointUris(“token”);.

But even though we can see this default configuration in the self-documentation, if we make a request to that endpoint, we will get an error. This is because the OpenIddict middleware still needs to be fully configured to handle requests to the discovery endpoint. In the next step, we will configure this to properly handle these requests.

The reason is that OpenIddict isn’t an out-of-the-box solution; rather, it’s designed to be a flexible solution that requires additional configuration steps. At a minimum, it requires a DB store configuration. Fortunately, an Entity Framework integration NuGet package is available from the same author as OpenIddict OpenIddict.EntityFrameworkCore. Even though it has its own drawbacks, which I’m going to cover later, it is still a very suitable starting point.

Store configuration

For the sample, I do a configuration using EFCore.UseInMemoryDatabase.

builder.Services.AddDbContext<DbContext>(
options =>
{
//in memory db provider for EF Core
options.UseInMemoryDatabase(nameof(DbContext));
//add OpenidDict entities
options.UseOpenIddict();
}
);

Now we are ready to move on with OpenIdDict configuration. The default entities, stores and managers be configured using the AddCore() configuration action. The entire sample configuration:

builder.Services.AddOpenIddict()
.AddCore(
_ =>
{
//register stores
_.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
}
)
.AddServer(
_ =>
{
//enable client_credentials grant_tupe support on server level
_.AllowClientCredentialsFlow();
//specify token endpoint uri
_.SetTokenEndpointUris("token");
//secret registration
_.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
_.DisableAccessTokenEncryption();
//the asp request handlers configuration itself
_.UseAspNetCore().EnableTokenEndpointPassthrough();
}
);

Client Registration

Once we configured stores, we are ready to proceed with client registration. Important to note that I’m not gonna discover here Applications (Clients) Management because it is a huge topic which is out of the introduction scope.

The Hosted Service is an appropriate spot to register test or preconfigured clients.

You may read more about Hosted Services in Asp Net Core:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-7.0&tabs=visual-studio

Adding client seeder:

using Microsoft.EntityFrameworkCore;
using OpenIddict.Abstractions;

namespace OpenIdDictSample
{
public class ClientSeeder : IHostedService
{
private readonly IServiceProvider _serviceProvider;

public ClientSeeder(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}


public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();

await PopulateScopes(scope, cancellationToken);

await PopulateInternalApps(scope, cancellationToken);
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

private async ValueTask PopulateScopes(IServiceScope scope, CancellationToken cancellationToken)
{
var scopeManager = scope.ServiceProvider.GetRequiredService<IOpenIddictScopeManager>();

var scopeDescriptor = new OpenIddictScopeDescriptor
{
Name = "test_scope",
Resources = { "test_resource" }
};

var scopeInstance = await scopeManager.FindByNameAsync(scopeDescriptor.Name, cancellationToken);

if (scopeInstance == null)
{
await scopeManager.CreateAsync(scopeDescriptor, cancellationToken);
}
else
{
await scopeManager.UpdateAsync(scopeInstance, scopeDescriptor, cancellationToken);
}
}

private async ValueTask PopulateInternalApps(IServiceScope scopeService, CancellationToken cancellationToken)
{
var appManager = scopeService.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();

var appDescriptor = new OpenIddictApplicationDescriptor
{
ClientId = "test_client",
ClientSecret = "test_secret",
Type = OpenIddictConstants.ClientTypes.Confidential,
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.Introspection,
OpenIddictConstants.Permissions.Endpoints.Revocation,

OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,

OpenIddictConstants.Permissions.Prefixes.Scope + "test_scope"
}
};

var client = await appManager.FindByClientIdAsync(appDescriptor.ClientId, cancellationToken);

if (client == null)
{
await appManager.CreateAsync(appDescriptor, cancellationToken);
}
else
{
await appManager.UpdateAsync(client, appDescriptor, cancellationToken);
}
}
}
}

And register our hosted service

builder.Services.AddHostedService<ClientSeeder>();

Now, if you try to make the same request, you will encounter another error. However, we can cover this as well.

Token issuing

One way to issue tokens with OpenIddict is through the token endpoint.

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using OpenIddict.Abstractions;

namespace OpenIdDictSample.Controllers
{
public class AuthorizationController : Controller
{
[HttpPost("~/token")]
public async ValueTask<IActionResult> Exchange()
{
//retrieve OIDC request from original request
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

if (request.IsClientCredentialsGrantType())
{
var clientId = request.ClientId;
var identity = new ClaimsIdentity(authenticationType: TokenValidationParameters.DefaultAuthenticationType);

identity.SetClaim(Claims.Subject, clientId);
identity.SetScopes(request.GetScopes());
var principal = new ClaimsPrincipal(identity);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}

throw new NotImplementedException("The specified grant type is not implemented.");
}
}

And voila, we have obtained our token.

Steps to verify the signature

Do you still remember that OIDC is self-documented? Well, now it could be very useful, .well-known/openid-configuration endpoint provides all necessary information including “jwks_uri”: “https://localhost:7153/.well-known/jwks", it refers to a URL that gives access to a JSON Web Key Set (JWKS), which contains all the public keys used to sign JWTs issued by the authorization server.
By accessing the JWKS URL, we can retrieve the public key corresponding to the private key used to sign our JWT. This allows us to verify the authenticity of the JWT and ensure that it has not been tampered with. We can do this by comparing the “kid” (key ID) parameter in the JWT header to the “kid” parameter in the JWKS, and using the corresponding public key to verify the signature.

In my case, it is:

 {
"kid": "803B0D015B74E45CDD27CF018DF83448FBE1863F",
"use": "sig",
"kty": "RSA",
"alg": "RS256",
"e": "AQAB",
"n": "6Ps8T8yINup4LESH32VidElJwDIOvx2a9hPxY6OyOmxRRorR-nqz5QhgSp1LRgxM9ReatTt1LQZ7VL1nEn633kfGP0viCJldUUw8UJkK85IFJSBMmFI-yKOCS48367kzYPeiIZHkj23EkP239bfhOCZny3czrc980-0sTaYGBjC0Djd4LF6gEV4U2hZ6hOrJu7bgr6MKFq05r02QOoJpQWB5bpdiX5aUX2KrJ7mt-SURrqaIDgbuDus1k-XnJq0g3DQuznLQjgAdzj6SVgkH1FElek58jgFPZJbB41HRQJophNNAWKSaGB350JebokP11fz3bBB1NCxm29hcD0LrsQ",
"x5t": "gDsNAVt05FzdJ88Bjfg0SPvhhj8",
"x5c": [
"MIIC9TCCAd2gAwIBAgIJAO7Ha1/4jUuXMA0GCSqGSIb3DQEBCwUAMDAxLjAsBgNVBAMTJU9wZW5JZGRpY3QgU2VydmVyIFNpZ25pbmcgQ2VydGlmaWNhdGUwHhcNMjMwMzIxMTAxMDU0WhcNMjUwMzIxMTAxMDU0WjAwMS4wLAYDVQQDEyVPcGVuSWRkaWN0IFNlcnZlciBTaWduaW5nIENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6Ps8T8yINup4LESH32VidElJwDIOvx2a9hPxY6OyOmxRRorR+nqz5QhgSp1LRgxM9ReatTt1LQZ7VL1nEn633kfGP0viCJldUUw8UJkK85IFJSBMmFI+yKOCS48367kzYPeiIZHkj23EkP239bfhOCZny3czrc980+0sTaYGBjC0Djd4LF6gEV4U2hZ6hOrJu7bgr6MKFq05r02QOoJpQWB5bpdiX5aUX2KrJ7mt+SURrqaIDgbuDus1k+XnJq0g3DQuznLQjgAdzj6SVgkH1FElek58jgFPZJbB41HRQJophNNAWKSaGB350JebokP11fz3bBB1NCxm29hcD0LrsQIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggEBANoA4tmOmSQu0fFPseVJyO/NEpZtGuVQGobYSCwfjd4EviT+wC7oJVXcLktm73oiQtKlg+DUajqQELHAcm9vteXxZ5iBRiTkaQ5k+anBM8OXtwgZFixDnhqJsWZxTSf+6LwFeWWqyP6wx/HhPlPwDttVSObIzem4YBqFZpLicHJQ1YjSF0/Dts8mfzbOqwotoAaa5ENUp7mQEtDcw/CJSuTxd6gouNj/TksV2+mjasVGdNPghVwWRlDp7TaD6YpMn9WIqmFDmEJLQtt8RgmBxirYqPgkMQlgCS/t9xqMnV/7JJycBschw4l0S+bsDnjB6nHzFBGag5AlD8NjP/QP4mY="
]
}

Let’s inspect our token with jwt.io

The way we did it any other application may verify such access_tokens.

Refresh tokens

As has already been described, refresh_tokens are used for getting new access_tokens.

Let’s add this ability to our server.

The first step is enabling refresh_tokens issuing.
In AddServer() method we need to add _.AllowRefreshTokenFlow()

Add the following into AuthorizationController.Exchange

if (request.IsRefreshTokenGrantType())
{
var claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;

return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}

Important that client that requests the refresh token should have OpenIddictConstants.Permissions.GrantTypes.RefreshToken in its Permissions:

Now, if the client requests also offline_access scope, it will receive refresh_token among with access_token

And now use it!
Replace grant_type with refresh_token and send the previous one.

That’s it! We’ve got our new pair of access_token and refresh_token.

Token Introspection

Even if we are using the signed self-contained JWT tokens, we may need to check token validity. Read more about [Token Introspection Endpoint].
Steps to enable this feature:
Add the following _.SetIntrospectionEndpointUris(“token/introspect”) inside AddServer(…).
And ensuring that we allowed it for our client:

Issue a new token and introspect it, you should see something like that:

Token Revocation

After issuing a token, we may need to revoke it due to security reasons, such as if it was stolen. This is particularly important for refresh tokens, which are long-lived objects and can provide unbreakable access to our protected resources.
This mechanism is easily achievable via a few configuration steps. We just need to define the endpoint by adding the following _.SetRevocationEndpointUris(“token/revoke”) inside AddServer(…).
And ensuring that we allowed it for our client:

Issue a token -> Introspect it -> Revoke it -> Introspect it one more time.
I’m revoking the token from the previous section:

And now the same Introspection request says that the token is invalid:

2 How, Why, and What if?

In this part, I would like to share not obvious things and underlying mechanisms that I consider very important to be mentioned.

Refresh Tokens Violation

While using the refresh_token grant_type, one might wonder what would happen if they request additional scopes that were not originally granted with the access_token. In such cases, an error would occur. However, if one tries to request fewer scopes, it would be successful. So if we can request fewer scopes why we can’t request more, and how does it work?
The reason behind this is that, during the creation of tokens, authorization is also generated and assigned to them. This authorization is attached to all other tokens that are requested through refresh_tokens, and it contains information about the original request. Therefore, if one tries to request fewer scopes, it would still be within the original authorization’s scope. However, requesting additional scopes would require new authorization, and the system would not allow it

Revocation in Details

I’ve already mentioned why we may need the revocation. And while it is quite simple with access_token, what if refresh_token is revoked? The answer is simple all related access tokens as well as related refresh_tokens, will be revoked. By related tokens, I mean tokens with the same authorisation. How does it work? — the same as in the case of refresh_token violation.

Refresh Tokens Rotation

You may have noticed that after requesting a new access_token via the refresh_token grant_type, we obtained a new pair of tokens where the refresh_token differed from the original one. But why? Security is a very important aspect and the same reasons we have the Revoke endpoint also apply here. If our refresh token is stolen, we need to detect it somehow and prevent malicious effects on our applications. How? This is where token rotation comes in.
By generating a new refresh token for each use and expecting to receive this new one for the next request, and marking already used tokens (OpenIdDict uses the “redeemed” status and stores it in the database to determine such tokens), we can handle suspicious behaviour in our application.
What if we try to use the same refresh_token twice? Once the system has tracked repeated usage of redeemed tokens, the system considers it as malicious behaviour and revokes all related tokens.
(The OpenIdDict maintains a “RedemptionDate” field to handle cases when two or more requests were sent to the very same time span and proceeds such requests as valid generating different refresh_tokens for each).

3 Enhancements

Here I would like to cover two moments we’ve implemented differently in our project.

Own db models

The First one refers to the default entities provided via OpenIddict.EntityFrameworkCore.

 public class OAuthEntityFrameworkCoreApplication :
OpenIddictEntityFrameworkCoreApplication<
int,
OAuthEntityFrameworkCoreAuthorization,
OAuthEntityFrameworkCoreToken
>
{
}

public class OAuthEntityFrameworkCoreAuthorization
: OpenIddictEntityFrameworkCoreAuthorization<
int,
OAuthEntityFrameworkCoreApplication,
OAuthEntityFrameworkCoreToken
>
{
}

public class OAuthEntityFrameworkCoreScope : OpenIddictEntityFrameworkCoreScope<int>
{
}

public class OAuthEntityFrameworkCoreToken
: OpenIddictEntityFrameworkCoreToken<
int,
OAuthEntityFrameworkCoreApplication,
OAuthEntityFrameworkCoreAuthorization
>
{
}

public static class OAuthEntityFrameworkCoreBuilder
{
public static OpenIddictEntityFrameworkCoreBuilder ReplaceWithCustomOAuthEntities(
this OpenIddictEntityFrameworkCoreBuilder builder
)
{
builder.ReplaceDefaultEntities<OAuthEntityFrameworkCoreApplication,
OAuthEntityFrameworkCoreAuthorization,
OAuthEntityFrameworkCoreScope,
OAuthEntityFrameworkCoreToken, int>();
return builder;
}
}

public static class DcOAuthEntityFrameworkCoreExtensions
{

public static DbContextOptionsBuilder UseCustomOAuth(this DbContextOptionsBuilder builder)
=> builder.UseOpenIddict<OAuthEntityFrameworkCoreApplication,
OAuthEntityFrameworkCoreAuthorization,
OAuthEntityFrameworkCoreScope,
OAuthEntityFrameworkCoreToken, int>();
}

and its usage

...
builder.Services.AddDbContext<DbContext>(
options =>
{
options.UseSqlServer("your connection string");

options.UseCustomOAuth();
}
);
...
builder.AddOpenIddict()
.AddCore(
options =>
{
options.UseEntityFrameworkCore()
.ReplaceWithCustomOAuthEntities()
.UseDbContext<DbContext>();
})
..

The only difference between this implementation and the original one is that we are using INT as ID type instead of original GUID.
Of course, it may not suit you, you maybe need to consider LONG (or even sortable GUIDs), but I highly recommend don’t use plain GUID.
There is a good article describing the reasons https://andrewlock.net/generating-sortable-guids-using-newid/.

Tokens Pruning

The second one refers to removing expired/invalid tokens and authorizations from your database.

You may use Quartz integration via OpenIddict.Quartz nuget package, to run a background job that will prune tokens. Under the hood, this is represented by the repeatable action which executes the following

IOpenIddictTokenManager.PruneAsync(...) 
and IOpenIddictAuthorizationManager.PruneAsync(...)

The cornerstone in integration with EFCore 6, it is really hard to create a productive query to remove data in EFCore 6 (but it has been fixed in EfCore 7).

If you use the default integration with EFCore (OpenIddict.EntityFrameworkCore), even author tried to optimize that you will get the following, I’ve obtained form logs.

Imagine that we have tokens, that should be removed. I’ve emulated this by manually setting ‘bad” expiration dates. And I have 8 expired tokens.

To query to retrieve them is:

Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (17ms) [Parameters=[@__p_1='?' (DbType = Int32), @__date_0='?' (DbType = DateTime2)], CommandType='Text', CommandTimeout='30']
SELECT TOP(@__p_1) [o].[Id], [o].[ApplicationId], [o].[AuthorizationId], [o].[ConcurrencyToken], [o].[CreationDate], [o].[ExpirationDate], [o].[Payload], [o].[Properties], [o].[RedemptionDate], [o].[ReferenceId], [o].[Status], [o].[Subject], [o].[Type]
FROM [OpenIddictTokens] AS [o]
LEFT JOIN [OpenIddictAuthorizations] AS [o0] ON [o].[AuthorizationId] = [o0].[Id]
WHERE ([o].[CreationDate] < @__date_0) AND ((([o].[Status] NOT IN (N'inactive', N'valid') OR ([o].[Status] IS NULL)) OR (([o0].[Id] IS NOT NULL) AND (([o0].[Status] <> N'valid') OR ([o0].[Status] IS NULL)))) OR ([o].[ExpirationDate] < GETUTCDATE()))

And It seems pretty good. but the query to delete:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (6ms) [Parameters=[@p0='?' (Size = 450), @p1='?' (Size = 50), @p2='?' (Size = 450), @p3='?' (Size = 50), @p4='?' (Size = 450), @p5='?' (Size = 50), @p6='?' (Size = 450), @p7='?' (Size = 50), @p8='?' (Size = 450), @p9='?' (Size = 50), @p10='?' (Size = 450), @p11='?' (Size = 50), @p12='?' (Size = 450), @p13='?' (Size = 50), @p14='?' (Size = 450), @p15='?' (Size = 50)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DELETE FROM [OpenIddictTokens]
WHERE [Id] = @p0 AND [ConcurrencyToken] = @p1;
SELECT @@ROWCOUNT;

DELETE FROM [OpenIddictTokens]
WHERE [Id] = @p2 AND [ConcurrencyToken] = @p3;
SELECT @@ROWCOUNT;

DELETE FROM [OpenIddictTokens]
WHERE [Id] = @p4 AND [ConcurrencyToken] = @p5;
SELECT @@ROWCOUNT;

DELETE FROM [OpenIddictTokens]
WHERE [Id] = @p6 AND [ConcurrencyToken] = @p7;
SELECT @@ROWCOUNT;

DELETE FROM [OpenIddictTokens]
WHERE [Id] = @p8 AND [ConcurrencyToken] = @p9;
SELECT @@ROWCOUNT;

DELETE FROM [OpenIddictTokens]
WHERE [Id] = @p10 AND [ConcurrencyToken] = @p11;
SELECT @@ROWCOUNT;

DELETE FROM [OpenIddictTokens]
WHERE [Id] = @p12 AND [ConcurrencyToken] = @p13;
SELECT @@ROWCOUNT;

DELETE FROM [OpenIddictTokens]
WHERE [Id] = @p14 AND [ConcurrencyToken] = @p15;
SELECT @@ROWCOUNT;

As you can see, deleting 8 tokens from db EF Core 6 generates 8 separate queries. So if you need to delete 1000 tokens it will generate 1000 separate queries and run them as butch. Doesn’t seem to be good, does it?

So we simply moved this operation into the DB Server side, by creating DB JOB [how to create database job].

That’s it!

The code from the article can be found on the GitHub.

If you enjoyed this story and found it insightful, be sure to follow me for more engaging content like this in the future.
You can also follow me on LinkedIn where I post notifications about my new publication with free access links.

In addition, if you build multitenant applications you may find this helpful: Multitenant OAuth Server with OpenIdDict and Multitenant Authorities for JWT Authentication in ASP

Thanks for your attention!

--

--