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.
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);
}
}