Search code examples
.net.net-coreoauth-2.0jwt

.NET Core API JwtBearerEvents.TokenValidated - race condition between multiple API requests


I'm overriding JwtBearerEvents.TokenValidated in order to match a User in my database against the Authenticated User (from a 3rd party authentication service - via JWT), and then populating some custom claims for me to use within my API (user subscription level, for example).

This code is tiggered from the Startup.cs, and is called for every API request. Within TokenValidated, I try to load the user associated with the token, and if they don't exist I create a User in the database.

However, I have the scenario that a client will make multiple requests against my API, and if this is a user that does not yet exist, the API creates multiple users - one for each time TokenValidated is called in parallel.

The code:

public class CustomJwtBearerEvents : JwtBearerEvents
{
    private IUsersRepository _usersRepository;

    public CustomJwtBearerEvents(IUsersRepository usersRepository)
    {
        this._usersRepository = usersRepository;
    }

    public override async Task TokenValidated(TokenValidatedContext context)
    {
        var subject = context.Principal.FindFirstValue("user_id");

        var dbUser = await _usersRepository.GetOrCreateUser(subject, context.Principal);

        // Populate custom claims
    }
}

I'm sure that I'm approaching this wrong, but I'm not sure how best to solve this problem - so this is my question:

How should I match 3rd Party authentication users to users in my database, whilst allowing concurrent API requests?


Solution

  • There are a couple of ways you could approach this. The most straight forward might be to implement a lock around the portion of the code that calls _usersRepository.GetOrCreateUser(subject, context.Principal); That would ensure only one thread at a time could access that line of code, preventing any dirty reads & duplicate inserts.

    That code would look something like this

    public class CustomJwtBearerEvents : JwtBearerEvents
    {
        private readonly object _key = new object();
        private IUsersRepository _usersRepository;
    
        public CustomJwtBearerEvents(IUsersRepository usersRepository)
        {
            this._usersRepository = usersRepository;
        }
    
        public override async Task TokenValidated(TokenValidatedContext context)
        {
            var subject = context.Principal.FindFirstValue("user_id");
    
            lock(_key)
            {
                var dbUser = await _usersRepository.GetOrCreateUser(subject, context.Principal);
            }
    
            // Populate custom claims
        }
    }
    

    As you might guess, this isn't going be the most performant, effectively making each of your API calls wait for all other calls to finish.

    To optimize a little bit, you might consider adding a GetUser() method to your IUsersRepository that can be used outside of the locked portion of the code.

    This would allow any requests for users that already exist to execute without waiting & only locking the code when a new user must be created.

    public class CustomJwtBearerEvents : JwtBearerEvents
    {
        private readonly object _key = new object();
        private IUsersRepository _usersRepository;
    
        public CustomJwtBearerEvents(IUsersRepository usersRepository)
        {
            this._usersRepository = usersRepository;
        }
    
        public override async Task TokenValidated(TokenValidatedContext context)
        {
            var subject = context.Principal.FindFirstValue("user_id");
    
            var dbUser = await _usersRepository.GetUser(subject, context.Principal);
    
            if (dbUser is null) 
            {
                lock(_key)
                {
                    dbUser = await _usersRepository.GetOrCreateUser(subject, context.Principal);
                }
            }
    
            // Populate custom claims
        }
    }
    

    All that said, the Microsoft.AspNet.Identity framework does a pretty good job of managing external user accounts, if that is something you are interested in checking out.

    Here is a link to their docs on the subject.