Reputation: 5389
I am trying to implement a simple api key based authentication handler. My handler method is
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Get the apiKey from a store...
if (apiKey != header.Parameter)
{
var error = "Invalid username or api key.";
return Task.FromResult(AuthenticateResult.Fail(error));
}
var claims = new List<Claim> {new Claim("user", (string)username)};
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, header.Scheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
When I make the request with the correct username and the api key, the method above returns AuthenticateResult.Success(ticket)
as expected. However, my controller action is not getting invoked despite being correctly authenticated. Instead, the Task HandleChallengeAsync(AuthenticationProperties properties)
is getting called and is returning 401 unauthorised response.
I'm registering my authentication handler in startup class like:
public void ConfigureServices(IServiceCollection services)
{
// register controllers, etc.
services.AddAuthentication("ApiKey").AddApiKeyBearer();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
IHostApplicationLifetime applicationLifetime)
{
app.ConfigureExceptionHandler()
.UseRouting()
.UseAuthentication()
.UseAuthorization()
.UseEndpoints(builder => builder.MapControllers());
}
How can I avoid the challenge since the authentication is already successful?
Upvotes: 1
Views: 6013
Reputation: 5389
I managed to find out the answer here. Basically, I needed to override the default authorization policy in the startup class like so
services.AddAuthorization(o =>
{
var builder = new AuthorizationPolicyBuilder("ApiKey");
builder = builder.RequireClaim("user");
o.DefaultPolicy = builder.Build();
});
Upvotes: 2
Reputation: 143
I think that it may be a little hard to pinpoint the problem without more code. I created an example that should work according to your requirements. Take a look at the example below.
Edit: If you do not want to configure the following setup manually you can take a look at the following library: https://github.com/mihirdilip/aspnetcore-authentication-apiKey/tree/3.1.1
Handler
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace WebApplication1.Handlers
{
public class ApiKeyAuthenticationSchemeOptions
: AuthenticationSchemeOptions
{ }
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
{
//TODO Change to whatever name you want to use
private const string ApiKeyHeaderName = "X-Token";
public ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey(ApiKeyHeaderName))
{
return Task.FromResult(AuthenticateResult.Fail("Header was not found"));
}
string token = Request.Headers[ApiKeyHeaderName].ToString();
//TODO Replace with proper token handling code
if (token == "secret")
{
Claim[] claims = new[] {
new Claim(ClaimTypes.NameIdentifier, "john123"),
new Claim(ClaimTypes.Email, "[email protected]"),
};
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, nameof(ApiKeyAuthenticationHandler));
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
else
{
return Task.FromResult(AuthenticateResult.Fail("Token is invalid"));
}
}
}
}
Startup
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using WebApplication1.Handlers;
namespace WebApplication1
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("ApiKey").AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>("ApiKey", op => { });
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Controller
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
namespace WebApplication1.Controllers
{
[Route("api/students")]
[ApiController]
public class StudentsController : ControllerBase
{
[HttpGet("secret")]
[Authorize]
public IActionResult GetData()
{
string email = User.Claims.ElementAt(1).Value;
return Ok("Secret data");
}
[HttpGet("public")]
public IActionResult GetData2()
{
return Ok("Public data");
}
}
}
Upvotes: 1