1

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.
3
  • Unless I'm mistaken, you've added the authentication service (services.AddAuthentication(...), but do you also need to configure the app to app.UseAuthentication()? Might be worth updating your code with the rest of the configuration. Commented Jul 12, 2019 at 0:20
  • @BrendanGreen Yes. I missed that. Updated. Still nothing. I guess I'll add those missing bits. I was hoping to avoid that. Commented Jul 12, 2019 at 18:58
  • Adding an AuthenticationScheme attribute to the Controller makes it fail. This is no good. It should fail by default. Commented Jul 12, 2019 at 20:32

2 Answers 2

2

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));
});
Sign up to request clarification or add additional context in comments.

5 Comments

This look promising, but it throws " Message=The type 'Microsoft.AspNetCore.Authorization.AuthorizeAttribute' must derive from 'Microsoft.AspNetCore.Mvc.Filters.IFilterMetadata'." I added 'using Microsoft.AspNetCore.Authorization;' Could that have been the wrong one?
I implemented this, see above, but it doesn't work.
@BWhite oops I think I gave you an old .netcore version's way to do it. Try that updated one.
No. InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. Thanks for trying.
RequireAuthenticatedUser doesn't feel right since there is no AuthenticatedUser using the API. I just want it to require the AuthenticationHandler.
0

The final resolution uses AddMVC. The solution was that in addition to adding a scheme, the scheme needed a requirement.

This works-ish. If I don't send any auth header, it returns an empty page. If I send an expired header, then it sends 500. Should be 401. But it doesn't return any values and that's really all I care about.

public class TokenAuthorizationRequirement: IAuthorizationRequirement {}

services.AddMvc(config => {
   var policy = new AuthorizationPolicyBuilder()
      .AddAuthenticationSchemes(new[] {BasicAuthenticationDefaults.AuthenticationScheme })
      .AddRequirements(new BasicAuthorizationRequirement())
      .Build();
   config.Filters.Add(new AuthorizeFilter(policy));
});

1 Comment

I'm marking @Matti-Price 's answer as the solution since I would never have thought to use AuthorizationPolicyBuilder so he has to get the credit.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.