Search code examples
c#.netauthenticationjwt

Using ApiKey and JWT token authentication in middleware .NET 6 application


I wanted to ask for advice for a specific approach using ApiKey and JWT token authentication. My .NET 6 application is like a middleware service, which gets a request from other service through HTTP. After that request, I am checking whether it has JWT token in Headers and if it has, I use it to validate the token. After validating it, I need to pass it to my other service class under the same project, there I create a HttpClient and I want to put that JWT token into it's header section. Unfortunately, I am unable to pass it through. I tried creating 'JwtTokenStore' class, store a JWT there and then pass it with dependency injection. I used AddTransient, but then realized that I got a 'null' value in my service, because it creates another instance of that 'IJwtTokenStore'.

I will give you some code snippets:

ApiKeyHandler:

protected override async Task<AuthenticateResult> HandleAuthenticationAsync()
{
    if (Request.Headers.TryGetValue(ApiKeyAuthenticationOptions.ApiKeyHeaderName, out var apiKeyHeaderValues))
    {
        headerKey = apiKeyHeaderValues.ToArray().FirstOrDefault();
        if (!string.IsNullOrEmpty(validKey) && !validKey.Equals(headerKey) && !validKey.Equals(uriKey))
        {
            return AuthenticateResult.NoResult();
        }
    }
    else
    {
        // check for JWT token
        var jwt = Request.Headers["Authorization"].FirstOrDefault(x => x.StartsWith("Bearer "));
        if (string.IsNullOrEmpty(jwt))
            return AuthenticateResult.Fail("No ApiKey or JWT token present in request headers.");

        // validate JWT token
        var tokenHandler = new JwtSecurityTokenHandler();
        
        var validationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            ValidateAudience = false,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey))
        };
        try
        {
            var jwtToken = tokenHandler.ReadJwtToken(jwt[7..]);
            var expClaim = jwtToken.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Exp)?.Value;
            validationParameters.ValidateLifetime = !string.IsNullOrEmpty(expClaim);

            tokenHandler.ValidateToken(jwt[7..], validationParameters, out SecurityToken validatedToken);
            // Maybe here I should store somewhere my JWT token if it's valid
        }
        catch
        {
            return AuthenticateResult.NoResult();
        }
    }
}

My custom service constructor where I want to add the JWT token if it's valid:

public CustomService(ILogger<CustomService> logger, IConfiguration configuration, IHttpClientFactory httpClientFactory)
{
    _logger = logger;
    _apiKey = configuration.GetValue<string>("ApiKey");
    _httpClient = httpClientFactory?.CreateClient() ?? new HttpClient();

    // here I should get that JWT token and check if it's not null, if null
    // then I use ApiKey
    if (!isApiKey || !string.IsNullOrEmpty(jwt))
    {
        _httpClient.DefaultRequestHeaders.Add("Authorization", jwt);
    }
    else //(!string.IsNullOrEmpty(_apiKey))
    {
        _httpClient.DefaultRequestHeaders.Add("X-Api-Key", _apiKey);
    }
}

Can you help me advicing how should I achieve this solution? Don't forget that this middleware application will be getting a lot of requests at the same time, so I need to know which JWT token should I send to my 3rd party service.

All the answers appreciated!

I've tried creating 'JwtTokenStore', put a JWT there and then try to get it in my custom service. After failing (because of AddTransient), I tried creating 'TokenQueue' class with ConcurrentQueue<string> and store the JWT there. But after sending two requests at the same time, in my 3rd party application it only receives the same token.

I saw an answer in this question: Can API Key and JWT Token be used in the same .Net 6 WebAPI but I also stuck at sending that JWT token forward to my custom service.

I also thought about a solution with Dictionary, that I should put token with some user's name or whatsoever, and then getting a token by that. But I am not sure if it's the best solution.


Solution

  • If anyone will be looking for a solution, here is what I did:

    ApiKeyHandler:

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        await Task.Delay(0);
    
        var endpoint = Context.Features.Get<IEndpointFeature>()?.Endpoint;
    
        if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
        {
            return AuthenticateResult.NoResult();
        }
    
        var validKey = _configuration.GetValue<string>("ApiKey");
        var uriKey = Request.Query.Where(x => x.Key == ApiKeyAuthenticationOptions.ApiKeyHeaderName).FirstOrDefault().Value.FirstOrDefault();
        var headerKey = null as string;
    
        if (Request.Headers.TryGetValue(ApiKeyAuthenticationOptions.ApiKeyHeaderName, out var apiKeyHeaderValues))
        {
            headerKey = apiKeyHeaderValues.ToArray().FirstOrDefault();
    
            if (!string.IsNullOrEmpty(validKey) && !validKey.Equals(headerKey) && !validKey.Equals(uriKey))
            {
                return AuthenticateResult.NoResult();
            }
    
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, "authenticated-client")
            };
    
            var identity = new ClaimsIdentity(claims, Options.AuthenticationType);
            var identities = new List<ClaimsIdentity> { identity };
            var principal = new ClaimsPrincipal(identities);
            var ticket = new AuthenticationTicket(principal, Options.Scheme);
    
            return AuthenticateResult.Success(ticket);
        }
        else if (Request.Headers.ContainsKey("Authorization") && AuthenticationHeaderValue.TryParse(Request.Headers["Authorization"], out var headerValue) && headerValue.Scheme == "Bearer")
        {
            var jwt = headerValue.Parameter;
    
            if (string.IsNullOrEmpty(jwt))
            {
                return AuthenticateResult.Fail("Invalid JWT token.");
            }
    
            try
            {
                var tokenHandler = new JwtSecurityTokenHandler();
                var token = tokenHandler.ReadJwtToken(jwt);
    
                var claims = new List<Claim>();
    
                foreach (var claim in token.Claims)
                {
                    claims.Add(new Claim(claim.Type, claim.Value));
                }
    
                var identity = new ClaimsIdentity(claims, Scheme.Name);
                var principal = new ClaimsPrincipal(identity);
                var ticket = new AuthenticationTicket(principal, Scheme.Name);
    
                if (!_httpContextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization"))
                {
                    _httpContextAccessor.HttpContext.Request.Headers["Authorization"] = new StringValues("Bearer " + jwt);
                }
    
                return AuthenticateResult.Success(ticket);
            }
            catch
            {
                return AuthenticateResult.Fail("Invalid JWT token.");
            }
        }
    
        return AuthenticateResult.Fail("Authentication failed.");
    }
    

    Then I created TokenService class:

    public class TokenService : ITokenService
    {
        readonly string? _token;
    
        public TokenService(IHttpContextAccessor httpContextAccessor) 
        {
            if (httpContextAccessor?.HttpContext?.Request?.Headers?.TryGetValue("Authorization", out var apiKeyHeaderValues) == true)
            {
                _token = apiKeyHeaderValues.FirstOrDefault();
            }
        }
    
        public bool IsTokenValid(string key, string token)
        {
            var mySecret = Encoding.UTF8.GetBytes(key);
            var mySecurityKey = new SymmetricSecurityKey(mySecret);
            var tokenHandler = new JwtSecurityTokenHandler();
            try
            {
                tokenHandler.ValidateToken(token,
                new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    ValidateIssuer = true,
                    ValidateAudience = false,
                    IssuerSigningKey = mySecurityKey,
                }, out SecurityToken validatedToken);
            }
            catch
            {
                return false;
            }
            return true;
        }
    
        public string? GetToken() => _token;
    

    And then, in my service where I want to use either api-key or jwt, I use the code below:

    public CustomService(
        ILogger<ISomeService> logger,
        IConfiguration configuration,
        IHttpClientFactory httpClientFactory,
        IMemoryCache memoryCache,
        ITokenService tokenService
        )
    {
        _logger = logger;
        _memoryCache = memoryCache;
        _baseUrl = configuration.GetValue<string>("Url")?.Trim('/', ' ') ?? throw new Exception("Url not set in configuration");
        _apiKey = configuration.GetValue<string>("ApiKey");
        _httpClient = httpClientFactory?.CreateClient() ?? new HttpClient();
    
        var jwt = tokenService?.GetToken();
    
        if (!string.IsNullOrEmpty(jwt))
        {
            _httpClient.DefaultRequestHeaders.Add("Authorization", jwt);
        }
        else
        {
            _httpClient.DefaultRequestHeaders.Add("X-Api-Key", _apiKey);
        }
    }