We have a centralized IdentityServer4-based authentication server to power all of our various applications. When a user accesses an application for the first time, a piece of middleware, UserProvisioningMiddleware
determines if the current user has been provisioned in the current application (effectively inserting some of their JWT claims into an application-specific database). Under rare circumstances, a race condition can occur where multiple requests are made simultaneously, the middleware is executed for each request, and it attempts to provision the user multiple times. This results in an internal server exception as the JWT sub
is used as the primary key.
An easy work-around would be placing a final check before saving the user to the database, and wrapping that call in a try-catch to silently discard the duplicate primary key error, but that is sub-optimal. A second potential solution would be to maintain a static HashSet
of all users currently undergoing provisioning, and to create Task
with a timer within my IsUserProvisioned
method that waits for the user to be dequeued and provisioned, but that still feels like it could cause some potential deadlocking.
Here is the implementation of my user provisioning service:
public class UserProvisioningService: IUserProvisioningService
{
private readonly IClaimsUserService _claimsUserService;
private readonly ICurrentUserService _currentUserService;
private readonly IJobSchedulingContext _context;
public UserProvisioningService(
IClaimsUserService claimsUserService,
ICurrentUserService currentUserService,
IJobSchedulingContext context)
{
_claimsUserService = claimsUserService;
_currentUserService = currentUserService;
_context = context;
}
/// <inheritdoc />
public Task<bool> IsProvisionedAsync()
{
var userId = _currentUserService.UserId;
if (userId == null)
throw new NotAuthorizedException();
return _context.Users
.NotCacheable()
.AnyAsync(u => u.Id == userId);
}
/// <inheritdoc />
public Task ProvisionAsync()
{
var userId = _currentUserService.UserId;
if (userId == null)
throw new NotAuthorizedException();
var claimsUser = _claimsUserService.GetClaimsUser();
if (claimsUser == null)
throw new InvalidOperationException("Cannot provision user");
var user = new ApplicationUser(
userId.Value,
claimsUser.GivenName,
claimsUser.FamilyName,
claimsUser.Email);
_context.Users.Add(user);
return _context.SaveChangesAsync();
}
}
And the middleware
public class UserProvisioningMiddleware
{
private readonly RequestDelegate _next;
public UserProvisioningMiddleware(RequestDelegate next)
{
_next = next;
}
[UsedImplicitly]
public async Task Invoke(
HttpContext context,
IUserProvisioningService provisioningService)
{
if (context.User?.Identity?.IsAuthenticated == true)
if (!await provisioningService.IsProvisionedAsync())
await provisioningService.ProvisionAsync();
await _next(context);
}
}
What are my other options to prevent this race condition from occurring again in the future, without sacrificing performance?