1

Dipping my toes for the very first time into the world of Web API and MVC.

Here's my program.cs:

namespace TelephoneInfoLookup;

using TelephoneInfoLookup.Middleware;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.DefaultIgnoreCondition =
            System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
    });

builder.Services.AddEndpointsApiExplorer()
    .AddSwaggerGen()
    .AddCors()
    .AddAuthentication("Basic");

var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseSwagger()
        .UseDeveloperExceptionPage()
        .UseSwaggerUI();
}
else
    app.UseHsts();

app.UseHttpsRedirection()
    .UseMiddleware<BasicAuthMiddleware>(new BasicAuthenticationOptions
    {
        Name = "Bob",
        Password = "My Uncle"
    })
    .UseAuthorization();

app.MapControllers();

app.Run();

And this is the middleware:

namespace TelephoneInfoLookup.Middleware;

using System.Net;
using System.Security.Principal;

public class BasicAuthMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly BasicAuthenticationOptions _options;
        public BasicAuthMiddleware(RequestDelegate next, BasicAuthenticationOptions options)
        {
            _next = next;
            _options = options ?? throw new ArgumentException("User info can't be null");
        }
        public async Task Invoke(HttpContext context)
        {
            if (CheckIsValidRequest(context, out string username))
            {
                var identity = new GenericIdentity(username);
                var principle = new GenericPrincipal(identity, null);
                context.User = principle;
                await _next.Invoke(context);
            }
            else
            {
                context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
            }
        }
        private bool CheckIsValidRequest(HttpContext context, out string username)
        {
            var basicAuthHeader = GetBasicAuthenticationHeaderValue(context);
            username = basicAuthHeader.UserName;
            return basicAuthHeader.IsValidBasicAuthenticationHeaderValue &&
                   basicAuthHeader.UserName == _options.Name &&
                   basicAuthHeader.Password == _options.Password;
        }
        private BasicAuthenticationHeaderValue GetBasicAuthenticationHeaderValue(HttpContext context)
        {
            var basicAuthenticationHeader = context.Request.Headers["Authorization"]
                .FirstOrDefault(header => header.StartsWith("Basic", StringComparison.OrdinalIgnoreCase));
            var decodedHeader = new BasicAuthenticationHeaderValue(basicAuthenticationHeader);
            return decodedHeader;
        }
    }
    public class BasicAuthenticationOptions
    {
        public string Name { get; set; }
        public string Password { get; set; }
    }
    public class BasicAuthenticationHeaderValue
    {
        public BasicAuthenticationHeaderValue(string authenticationHeaderValue)
        {
            if (!string.IsNullOrWhiteSpace(authenticationHeaderValue))
            {
                _authenticationHeaderValue = authenticationHeaderValue;
                if (TryDecodeHeaderValue())
                {
                    ReadAuthenticationHeaderValue();
                }
            }
        }
        private readonly string _authenticationHeaderValue;
        private string[] _splitDecodedCredentials;
        public bool IsValidBasicAuthenticationHeaderValue { get; private set; }
        public string UserName { get; private set; }
        public string Password { get; private set; }
        private bool TryDecodeHeaderValue()
        {
            const int headerSchemeLength = 6;
            if (_authenticationHeaderValue.Length <= headerSchemeLength)
            {
                return false;
            }
            var encodedCredentials = _authenticationHeaderValue.Substring(headerSchemeLength);
            try
            {
                var decodedCredentials = Convert.FromBase64String(encodedCredentials);
                _splitDecodedCredentials = System.Text.Encoding.ASCII.GetString(decodedCredentials).Split(':');
                return true;
            }
            catch (FormatException)
            {
                return false;
            }
        }
        private void ReadAuthenticationHeaderValue()
        {
            IsValidBasicAuthenticationHeaderValue = _splitDecodedCredentials.Length == 2
                                                   && !string.IsNullOrWhiteSpace(_splitDecodedCredentials[0])
                                                   && !string.IsNullOrWhiteSpace(_splitDecodedCredentials[1]);
            if (IsValidBasicAuthenticationHeaderValue)
            {
                UserName = _splitDecodedCredentials[0];
                Password = _splitDecodedCredentials[1];
            }
        }
    }

With this I'm able to ensure that only user Bob with the password of My Uncle can use this endpoint:

namespace TelephoneInfoLookup.Controllers;

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("[Controller]")]
[Produces("application/json")]
[Authorize]
public class LookupController : ControllerBase
{

    private readonly ILogger<LookupController> _logger;

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

    [HttpGet]
    public async Task<Lookup> Get(string telephoneNumber, string fields)
    {
        var ret = await Lookup.LookupDetails(telephoneNumber, fields);
        return ret;
    }
}

However another method that I want to be left open still demands I use basic authentication and I get a 401 whenever I try to call it:

namespace TelephoneInfoLookup.Controllers;

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[AllowAnonymous]
[ApiController]
[Route("Simple")]
public class SimpleLookupController : ControllerBase
{

    private readonly ILogger<LookupController> _logger;

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

    [HttpGet]
    [AllowAnonymous]
    public async Task<string> GetExtension(string telephoneNumber)
    {
        var lookup = await Lookup.LookupDetails(telephoneNumber, null);
        var ret = lookup!.VIPExtension!;
        return string.IsNullOrEmpty(ret) ? "" : ret;
    }
}

I tried things like adding

app.UseHttpsRedirection()
    .UseMiddleware<BasicAuthMiddleware>(new BasicAuthenticationOptions
    {
        Name = "Bob",
        Password = "Your Uncle"
    })
    .UseAuthentication()
    .UseAuthorization();

but to no avail.

What silly thing am I missing in this?

I suspect it's something in the Middleware that needs to be changed, but since I'm totally green at all this, I'm rather perplexed.

I did spend some time trying to get my head around AllowAnonymous not working with Custom AuthorizationAttribute which would lead me to think I may be in need of utilizing something using System.Web.Http.AuthorizeAttribute but I'm kinda lost how I'd use that in my code (well, not my code, someone else's that I found that got my basic authentication "working"!)

Rachel Ambler
  • 1,440
  • 12
  • 23
  • In a line by line debug, with a breakpoint in the middleware, what did you find? – Leandro Bardelli Mar 25 '23 at 00:37
  • It going through the motions of trying to authenticate. Then failing and spitting out the 401. – Rachel Ambler Mar 25 '23 at 01:02
  • I believe the middleware will work everytime, maybe the condition has to be in the middleware – Leandro Bardelli Mar 25 '23 at 01:03
  • That's what I was getting from the linked question. I just don't know how, though. – Rachel Ambler Mar 25 '23 at 01:16
  • Well you can work with sessions and HttpContext and extract if the user has or has not a context or authenticated token. (user can be another system) is a functional way more than a code way. You can check also if the the context has some rol in the session, and dont ask for that role (anonymous role) – Leandro Bardelli Mar 25 '23 at 01:30

1 Answers1

0

Found the answer!

I needed to add the following into my middleware:

public async Task Invoke(HttpContext context)
{
    var endpoint = context.GetEndpoint();
    if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() is object)
    {
        await _next(context);
        return;
    }

Source: https://www.stevejgordon.co.uk/anonymous-aware-middleware-with-endpoint-routing-and-healthchecks

Rachel Ambler
  • 1,440
  • 12
  • 23