- Home
- .NET tutorials
- How to secure ASP.NET Core APIs with Basic Authentication
How to secure ASP.NET Core APIs with Basic Authentication
Published: Monday 2 March 2026
Developers will often tell you that you need Entra, Identity Server, or JWT to secure your ASP.NET Core Web API.
But for most internal APIs, that is massive overkill.
This tutorial shows you how to add a Basic Authentication handler and wire it into ASP.NET Core Web APIs.
Create the authentication handler
The first task is to create a class that inherits from AuthenticationHandler.
You also need to add a constructor with the required parameters and call the base class constructor.
// BasicAuthenticationHandler.cs
public class BasicAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string SchemeName = "BasicAuthentication";
public BasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder) { }
}Failure points
Authentication handlers are effectively bouncers. Every AuthenticateResult.Fail(...) is a reason someone does not get in.
In this handler, we have four failure points.
1. Missing Authorization request header
Basic Authentication requires an Authorization request header.
If it is missing, we can fail authentication immediately.
// BasicAuthenticationHandler.cs
public class BasicAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
...
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue("Authorization", out var authHeaderValue))
{
return Task.FromResult(AuthenticateResult.Fail(
"'Authorization' is missing from the request header"));
}
...
}
}2. Cannot parse to AuthenticationHeaderValue
If the Authorization header exists, we should be able to parse it as an AuthenticationHeaderValue.
This stores the scheme (for example, Basic) and the parameter (the credentials).
// BasicAuthenticationHandler.cs
public class BasicAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
...
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
...
if (!AuthenticationHeaderValue.TryParse(
authHeaderValue.ToString(), out var authHeader))
{
return Task.FromResult(AuthenticateResult.Fail(
"Unable to convert to an authentication header value"));
}
...
}
}3. Not using the Basic scheme
The Basic Authentication header consists of the word Basic followed by a Base64-encoded string of username:password.
Basic cm91bmR0aGVjb2RlOnJvdW5kdGhlY29kZQ==The scheme is case-insensitive, so basic is also valid. If the scheme is not Basic, authentication fails.
// BasicAuthenticationHandler.cs
public class BasicAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
...
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
...
if (!authHeader.Scheme.Equals("Basic",
StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(AuthenticateResult.Fail(
"Authentication scheme is not 'Basic'"));
}
...
}
}4. Invalid username or password
The final check validates the credentials.
We decode the Base64 string, which should produce a colon-separated username:password value.
Base64 encoded string:
cm91bmR0aGVjb2RlOnJvdW5kdGhlY29kZQ==Base64 decoded string:
roundthecode:roundthecode
This step includes several checks:
Is the value valid Base64?
Does the decoded value contain a colon?
Is the username and password correct?
If any check fails, authentication fails.
// BasicAuthenticationHandler.cs
public class BasicAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
...
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
...
if (!Base64.IsValid(authHeader.Parameter!))
{
return Task.FromResult(AuthenticateResult.Fail(
"'Authorization' header value is not formatted correctly"));
}
var credentialsDecoded = Encoding.UTF8.GetString(
Convert.FromBase64String(authHeader.Parameter!));
var credentials = credentialsDecoded.Split(':', 2);
if (credentials.Length != 2)
{
return Task.FromResult(AuthenticateResult.Fail(
"'Authorization' header value is not formatted correctly"));
}
var username = credentials[0];
var password = credentials[1];
if (username != "roundthecode" || password != "roundthecode")
{
return Task.FromResult(AuthenticateResult.Fail(
"Invalid username or password"));
}
...
}
}Authentication is successful
If all checks pass, authentication succeeds.
The next step is to describe who the user is. This is done using claims.
// BasicAuthenticationHandler.cs
public class BasicAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
...
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
...
var identity = new ClaimsIdentity(
new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.AuthenticationMethod, authHeader.Scheme)
},
SchemeName);
...
}
}At this point, ASP.NET Core considers the request authenticated.
Authorisation policies can run, and your endpoint code executes.
// BasicAuthenticationHandler.cs
public class BasicAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
...
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
...
return Task.FromResult(AuthenticateResult.Success(
new AuthenticationTicket(
new ClaimsPrincipal(identity),
SchemeName)));
}
}Here is the full BasicAuthenticationHandler.cs code
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string SchemeName = "BasicAuthentication";
public BasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder) : base(options, logger, encoder) { }
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue("Authorization",
out var authHeaderValue))
{
return Task.FromResult(AuthenticateResult.Fail(
"'Authorization' is missing from the request header")
);
}
if (!AuthenticationHeaderValue.TryParse(authHeaderValue.ToString(), out var authHeader))
{
return Task.FromResult(AuthenticateResult.Fail(
"Unable to convert to a authentication header value"));
}
if (!authHeader.Scheme.Equals("Basic",
StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(AuthenticateResult.Fail(
"Authentication scheme is not 'Basic'"));
}
if (!Base64.IsValid(authHeader.Parameter!))
{
return Task.FromResult(AuthenticateResult.Fail(
"'Authorization' header value isn't formatted correctly")
);
}
var credentialsDecoded = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader.Parameter!));
var credentials = credentialsDecoded.Split(':', 2);
if (credentials.Length != 2)
{
return Task.FromResult(AuthenticateResult.Fail(
"'Authorization' header value isn't formatted correctly")
);
}
var username = credentials[0];
var password = credentials[1];
if (username != "roundthecode" || password != "roundthecode")
{
return Task.FromResult(AuthenticateResult.Fail(
"Invalid username or password")
);
}
var identity = new ClaimsIdentity(
[
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.AuthenticationMethod, authHeader.Scheme)
],
SchemeName
);
return Task.FromResult(AuthenticateResult.Success(
new AuthenticationTicket(
new ClaimsPrincipal(identity),
SchemeName
)));
}
}Taking a deeper dive into authentication handlers
Now that we have our Basic Authentication handler, the next step is to register it with the API so it actually works.
If you want a deeper dive into creating authentication handlers from scratch, I cover it in my Minimal APIs for complete beginners course.
But for this tutorial, let's continue and see how to register the handler so it protects the endpoints.
Add a policy and register the authentication handler
This is the part many developers get wrong.
You must:
Register the policy using
AddAuthorizationRegister the authentication scheme using
AddAuthentication
// ConfigureServices.cs
public static class ConfigureServices
{
extension(IServiceCollection services)
{
public IServiceCollection AddBasicAuthentication()
{
services.AddAuthorization(options =>
{
options.AddPolicy(BasicAuthenticationHandler.SchemeName, policy =>
{
policy.AddAuthenticationSchemes(
BasicAuthenticationHandler.SchemeName);
policy.RequireAuthenticatedUser();
});
});
services.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(
BasicAuthenticationHandler.SchemeName, null);
return services;
}
}
}// Program.cs
builder.Services.AddBasicAuthentication();Use with Minimal APIs
With Minimal APIs, call RequireAuthorization on a route group or endpoint.
If you only have one policy, passing the scheme is optional, but specifying it makes your intent explicit and supports multiple schemes later.
// ProductsEndpoints.cs
public static class ProductsEndpoints
{
extension(WebApplication app)
{
public WebApplication MapProductsEndpoints()
{
var group = app.MapGroup("/api/products")
.RequireAuthorization(BasicAuthenticationHandler.SchemeName);
group.MapGet("{id}", GetProduct);
return app;
}
}
public static GetProductDto GetProduct(int id)
{
return new GetProductDto(id, "Television");
}
}// Program.cs
app.MapProductsEndpoints();Use with controllers
The authentication handler does not change for controllers.
First, create a custom AuthorizeAttribute and set the authentication scheme.
// BasicAuthorizationAttribute.cs
public class BasicAuthorizationAttribute : AuthorizeAttribute
{
public BasicAuthorizationAttribute()
{
AuthenticationSchemes =
BasicAuthenticationHandler.SchemeName;
}
}Then apply it to the endpoints you want to protect.
// CategoriesController.cs
[Route("api/categories")]
[ApiController]
public class CategoriesController : ControllerBase
{
[HttpGet("{id}")]
[BasicAuthorization]
public GetCategoryDto Get(int id)
{
return new GetCategoryDto(id, "Electronics");
}
}Watch the video
Watch the video where we test Basic Authentication in Postman using a mix of invalid and valid credentials.
And if you want to try it out for yourself, you can download the code example.
Related tutorials
What is JWT and how to add it to ASP.NET Core
How to use JWT in ASP.NET Core for Bearer token authentication and security within the OAuth Client Credentials flow.