Search code examples
c#asp.net-coreasp.net-web-apiasp.net-core-webapibasic-authentication

How do I set up basic authentication on specific actions in my asp net core 3.1 controller?


I am creating a webapi using asp.net core 3.1 and I want to use basic authentication, which I will authenticate against Active Directory.

I create an authentication handler and service, but the problem is that when I decorate my controller action with [Authorize], the HandleAuthenticateAsync function is not called when I invoke the controller action (although the hander's constructor is called). Instead I just get a 401 response:

GET https://localhost:44321/Test/RequiresAuthentication HTTP/1.1
Authorization: Basic ..........
User-Agent: PostmanRuntime/7.26.8
Accept: */*
Postman-Token: d58490e4-2707-4b75-9cfa-679509951860
Host: localhost:44321
Accept-Encoding: gzip, deflate, br
Connection: keep-alive



HTTP/1.1 401 Unauthorized
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Date: Thu, 10 Dec 2020 16:31:16 GMT
Content-Length: 0

And if I call a the action that does not have the [Authorize] attribute, the HandleAuthenticateAsync function is called, but the action executes and returns a 200 even if the HandleAuthenticateAsync returned a AuthenticateResult.Fail. I must be totally misunderstanding how this is supposed to work.

GET https://localhost:44321/Test/NoAuthenticationRequired HTTP/1.1
User-Agent: PostmanRuntime/7.26.8
Accept: */*
Postman-Token: 81dd4c2a-32b6-45b9-bb88-9c6093f3675e
Host: localhost:44321
Accept-Encoding: gzip, deflate, br
Connection: keep-alive


HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Vary: Accept-Encoding
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Date: Thu, 10 Dec 2020 16:35:36 GMT
Content-Length: 3

Ok!

I have a Controller with one action that I want to be authenticated on, and one I don't:

[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{

    private readonly ILogger<TestController> _logger;

    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }

    [HttpGet("RequiresAuthentication")]
    [Authorize]
    public string RestrictedGet()
    {
        return "Ok!";
    }

    [HttpGet("NoAuthenticationRequired")]
    public string NonRestrictedGet()
    {
        return "Ok!";
    }
}

I have an authentication handler:

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private IBasicAuthenticationService _authService;

    public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IBasicAuthenticationService authService)
            : base(options, logger, encoder, clock)
    {
        ...
        _authService = authService;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // skip authentication if endpoint has [AllowAnonymous] attribute
        var endpoint = Context.GetEndpoint();

        if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
        {
            return AuthenticateResult.NoResult();
        }

        if (!Request.Headers.ContainsKey("Authorization"))
        {
            return AuthenticateResult.Fail("Missing Authorization Header.");
        }

        IBasicAuthenticationServiceUser user = null;
        try
        {
            var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
            var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
            var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
            var username = credentials[0];
            var password = credentials[1];
            user = await _authService.Authenticate(username, password);
        }
        catch
        {
            return AuthenticateResult.Fail("Invalid Authorization Header.");
        }

        if (user == null)
        {
            return AuthenticateResult.Fail("Invalid Username or Password.");
        }

        var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.UserName),
            };

        var identity = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);

        return AuthenticateResult.Success(ticket);
    }
}

I have an authentication service that authenticates against active directory:

class BasicAuthenticationActiveDirectoryService : IBasicAuthenticationService
{
    private class User : IBasicAuthenticationServiceUser
    {
        public string Id { get; set; }
        public string UserName { get; set; }
        public string Lastname { get; set; }
        public string FirstName { get; set; }
        public string Email { get; set; }
    }

    public async Task<IBasicAuthenticationServiceUser> Authenticate(string username, string password)
    {
        string domain = GetDomain(username);

        using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, domain))
        {
            // validate the credentials
            bool isValid = pc.ValidateCredentials(username, password);

            if (isValid)
            {
                User user = new User()
                {
                    Id = username,
                    UserName = username
                };

                UserPrincipal up = UserPrincipal.FindByIdentity(pc, username);

                user.FirstName = up.GivenName;
                user.Lastname = up.Surname;
                user.Email = up.EmailAddress;

                return user;
            }
            else
            {
                return null;
            }
        }
    }

    private string GetDomain(string username)
    {
        if (string.IsNullOrEmpty(username))
        {
            throw new ArgumentNullException(nameof(username), "User name cannot be null or empty.");
        }

        int delimiter = username.IndexOf("\\");
        if (delimiter > -1)
        {
            return username.Substring(0, delimiter);
        }
        else
        {
            return null;
        }
    }
}

I wire this up in my startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // configure DI for application services
    services.AddScoped<IBasicAuthenticationService, BasicAuthenticationActiveDirectoryService>();

    //Set up basic authentication
    services.AddAuthentication("BasicAuthentication")
        .AddScheme<Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

    ...
}

I setup authentication and authorization in my startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

   ...

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();
    app.UseAuthentication();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Solution

  • Try to swap the order of app.UseAuthorization() and app.UseAuthentication(). Because UseAuthentication will parse the token, and then extract the user's information to HttpContext.User. Then UseAuthorization will authorize based on the authentication scheme.