I'm creating an authentication scheme for my ASP.NET Core API.
It calls my handler and hits the breakpoint just fine, but the API calls still return results even when the authorization fails.
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey(AuthorizationHeaderName))
{
//Authorization header not in request
return AuthenticateResult.Fail("Missing Authorization header");
}
In my naive understanding, it shouldn't return data if it fails authentication.
What am I missing?
DETAILS
I register the scheme like this in Startup.ConfigureServices
services.AddAuthentication(options => {
// This (options.Default..Scheme) causes the default authentication scheme to be set.
// Without this, the Authorization header is not checked and
// you'll get no results.
options.DefaultAuthenticateScheme = BasicAuthenticationDefaults.AuthenticationScheme;
}).AddScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>("Basic", null);
Startup.Config calls
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseMvc();
The rest of the code looks like this:
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace WebAPI.Authentication
{
public interface IBasicAuthenticationService
{
Task<AuthenticateResult> HandleAuthenticateAsync();
}
public static class BasicAuthenticationDefaults
{
public const string AuthenticationScheme = "Basic";
}
public class BasicAuthenticationOptions : AuthenticationSchemeOptions
{ }
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
private const string AuthorizationHeaderName = "Authorization";
private const string BasicSchemeName = "Basic";
public BasicAuthenticationHandler(
IOptionsMonitor<BasicAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey(AuthorizationHeaderName))
{ // Rejected here. Should fail.
//Authorization header not in request
return AuthenticateResult.Fail("Missing Authorization header");
}
if .... // never gets this far
}
return AuthenticateResult.Success(ticket);
}
}
}
And here is the controller that is improperly returning results.
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace TMAWebAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
}
}
All these lines of code get hit in the debugger, so that part seems to be working correctly.
But the API call is still returning results even though it fails authentication.
Update:
Adding an AuthenticationScheme attribute to the Controller makes it fail.
Like this:
[Route("api/[controller]")]
[ApiController]
[Authorize(AuthenticationSchemes = "Basic")]
public class ValuesController : ControllerBase
This is no good. It should fail by default instead of having to add it to every controller.
Update 2:
Adding a filter to services.AddMvc looks promising, but that doesn't work either. Documentation claims that you don't have to implement an Authorization filter since they are included. Not that I can find.
I inherited from AuthorizeAttribute, using the idea from Matti Price, and IFilterMetadata, required by AddMvc. That compiles but still allows unauthorized access.
public class BasicAuthorizeAttribute : AuthorizeAttribute, IFilterMetadata { }
services.AddMvc(options => {
options.Filters.Add(typeof(BasicAuthorizeAttribute));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Update 3:
Tried
policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser()
as suggested by Matti, but that returned
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
I don't have any interest in redirecting to a non-existent login page for an API, so I tried
policy = new AuthorizationPolicyBuilder().AddAuthenticationSchemes(new[] {BasicAuthenticationDefaults.AuthenticationScheme })
That compiles but throws the exception
InvalidOperationException Message=AuthorizationPolicy must have at least one requirement.
You'll need to add the [Authorize]
attribute to your controllers to cause the authorization to actually do anything with it's result. You can add it globally like this :
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});