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"!)