0

This is how I create JWT tokens for my .NET Core API and it's working perfectly fine, but I'd like to implement the possibility to revoke, disable or invalidate JWT tokens when an HTTP request comes asking for it, with the token in the header.

I can think of a way where I would store the token in my database and have a boolean column indicating whether the token is active or not, but is there a way to do it without storing the token in the database at all?

public UserAuthenticationResponse CreateToken(UserAuthenticationRequest userAuth)
{
    var user = // try to find user in database...
    if (user == null)
    {
        return null;
    }

    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new Claim[]
        {
            new Claim("user_id", userAuth.Id),
        }),
        Expires = DateTime.UtcNow.AddMinutes(5),
        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
    };

    var token = tokenHandler.CreateToken(tokenDescriptor);
    var authenticatedUser = new UserAuthenticationResponse();
    authenticatedUser.Id = user.Id;
    authenticatedUser.Token = tokenHandler.WriteToken(token);

    return authenticatedUser;
}
TheDoomDestroyer
  • 2,434
  • 4
  • 24
  • 45
  • Firstly consider if jwt is the best form of authorization for you, then have a read of https://stackoverflow.com/questions/21978658/invalidating-json-web-tokens – Kevin Oct 11 '19 at 14:27

1 Answers1

1

What I did in the end was create middleware which I put at the top of my pipeline. I held a List<string> that represented a list of blacklisted tokens (which I later clean up once they expire).

When I were validating the token myself, I was checking the BlacklistToken(token, timeout) method. If it returns true, I can blacklist the token and the next time the user tries to access something with that token, it won't let him do it. Then, sometime later, I call CleanupTokens(tokenProvider) which gets all tokens, checks if they're expired (thanks to the tokenProvider that gets the token expiration date) and if so, it removes them from the list.

public class TokenPair
{
    [JsonProperty(PropertyName = "token")]
    public string Token { get; set; }
    [JsonProperty(PropertyName = "userId")]
    public string UserID { get; set; }
}

public interface ITokenLocker
{
    bool BlacklistToken(string token, int timeout);
    void CleanupTokens(ITokenProvider tokenProvider);
    bool IsBlacklisted(string token);
}

public class TokenLocker : ITokenLocker
{
    private List<string> _blacklistedTokens;

    public TokenLocker()
    {
        _blacklistedTokens = new List<string>();
    }

    public bool BlacklistToken(string token, int timeout)
    {
        lock (_blacklistedTokens)
        {
            if (!_blacklistedTokens.Any(x => x == token))
            {
                _blacklistedTokens.Add(token);
                return true;
            }
        }

        Thread.Sleep(timeout);

        lock (_blacklistedTokens)
        {
            if (!_blacklistedTokens.Any(x => x == token))
            {
                _blacklistedTokens.Add(token);
                return true;
            }
            else
                return false;
        }
    }

    public void CleanupTokens(ITokenProvider tokenProvider)
    {
        lock (_blacklistedTokens)
        {
            for (int i = 0; i < _blacklistedTokens.Count; i++)
            {
                var item = _blacklistedTokens[i];

                DateTime expiration = tokenProvider.GetExpiration(item);
                if (expiration < DateTime.UtcNow)
                {
                    _blacklistedTokens.Remove(item);
                    i--;
                }
            }
        }
    }

    public bool IsBlacklisted(string token)
    {
        return _blacklistedTokens.Any(tok => tok == token);
    }
}

public class TokenMiddleware
{
    private readonly RequestDelegate _next;

    public TokenMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    private string GetToken(HttpContext ctx)
    {
        ctx.Request.EnableBuffering();
        using (var reader = new StreamReader(ctx.Request.Body, Encoding.UTF8, true, 1024, true))
        {
            var jsonBody = reader.ReadToEnd();
            var body = JsonConvert.DeserializeObject<TokenPair>(jsonBody);
            ctx.Request.Body.Position = 0;
            if (body != null && body.Token != null)
            {
                return body.Token;
            }
        }
        return string.Empty;
    }

    public async Task InvokeAsync(HttpContext context,
        ITokenLocker tokenLocker
        )
    {
        var ctx = context;
        if (tokenLocker.IsBlacklisted(GetToken(ctx)))
        {
            int statusCode = (int)HttpStatusCode.Unauthorized;
            ctx.Response.StatusCode = statusCode;
            var response = FaziHttpResponse.Create(statusCode, "Unauthorized: Invalid / Expired token");
            await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
            return;
        }

        await _next(context);
    }
}
TheDoomDestroyer
  • 2,434
  • 4
  • 24
  • 45