Back to: ASP.NET Core Web API Tutorials
How to Implement Basic Authentication in ASP.NET Core Web API
In this article, I will discuss how to implement Basic Authentication in ASP.NET Core Web API with an example. Please read our previous article discussing RSA Asymmetric Encryption in ASP.NET Core Web API with an Example.
Basic Authentication in ASP.NET Core Web API
Basic Authentication is a straightforward method for securing Web APIs, where clients provide a username and password in the request header. This method is widely supported and easy to implement. Before understanding and Implementing Basic Authentication in ASP.NET Core Web API, let us first understand what Authentication and Authorization, why we need authentication, and what are different authentication mechanisms are available in ASP.NET Core.
What is Authentication?
Authentication is the process of confirming and verifying a user’s identity. In a technical context, it ensures that a user is who they claim to be. When a user attempts to access a system, the system checks their credentials (e.g., username and password) against a data source, such as a database, to confirm their identity. Only after successful authentication, the system will allow access to its resources. So, authentication answers the question, who are you? For a better understanding, please have a look at the following image:
Let us understand Authentication from a layman’s point of view. Imagine entering a secured office campus. At the gate, there’s a biometric scanner. Employees must verify their identity via the sensor (e.g., by scanning their fingerprints). When an employee scans their fingerprint, the system checks it against its database. If it matches, access is granted. Similarly, in a web application, credentials are validated before allowing access to the system. This verification step is exactly what we call Authentication.
What is Authorization?
Once a user is authenticated, the next step is Authorization, which determines what resources or actions an authenticated user can access. While authentication verifies the user’s identity, authorization ensures the user has the appropriate permissions to access specific resources. It answers the question, what are you allowed to do? For a better understanding, please have a look at the following image:
Imagine a company building with restricted access to different areas (like HR, Finance, Server Room, etc.). After the biometric verifies the employee’s identity, enter the Campus. Then, the employee’s role (e.g., Manager, Developer, HR) determines which areas they are authorized to enter. For example:
- HR staff can access HR files but not the server room.
- IT administrators may access all areas.
The role and privileges assigned to the user decide what areas they can access, which is the authorization process. In web applications, authorization mechanisms ensure users can only access resources or perform actions based on their roles.
Note: Authorization always comes after Authentication. You must first verify who the user is before determining what they can access.
Why do we need Authentication in ASP.NET Core Web API?
In the context of Web APIs, authentication plays a crucial role in maintaining the integrity and security of the system. Web APIs typically operate under the REST (Representational State Transfer) architectural style. One of the core principles of REST (Representational State Transfer) is that the communications must be stateless. This means the server doesn’t retain any information about the client between requests. Each HTTP request from a client to a server must contain all the necessary information to understand and process the request. The server does not retain any session information about the client between requests.
Given the stateless nature, the server does not maintain any session state, so every request looks new to the server. Therefore, each request must contain all the information required for authentication and authorization. Authentication in Stateless Systems works as follows:
- The client must send credentials (like tokens or username/password pairs) with every request.
- The server validates these credentials against a database or other storage.
- If valid, the server processes the request; otherwise, it responds with a 401 Unauthorized status.
For Example, a mobile app sends a request to fetch user profile data. With no stored session, the app includes the user’s credentials (or a token) in every request. The server then verifies these credentials to confirm the user’s identity before returning the data. This mechanism ensures that every interaction is secure and that only legitimate users can access the API resources.
Types of Authentications in Web Services:
ASP.NET Core Web API supports various authentication mechanisms, each suited to different scenarios and security requirements. Some of the common types include:
- Basic Authentication: This transmits the username and password with each request, encoded in Base64.
- Token-Based Authentication: Uses tokens like JWT (JSON Web Tokens) that clients obtain after initial authentication, reducing the need to send credentials repeatedly.
- OAuth/OpenID Connect: A robust framework for delegated authorization, commonly used for third-party integrations.
Note: In this session, we will discuss implementing Basic Authentication in the ASP.NET Core Web API Application.
What is Basic Authentication in ASP.NET Core Web API?
Basic Authentication is one of the simplest methods of securing API endpoints. With Basic Authentication, the client sends a Username and Password with each request, typically encoded in Base64 format. These credentials are usually sent in the HTTP Authorization header, encoded as a Base64 string. In ASP.NET Core Web API, Basic Authentication is used to verify the identity of a user by validating the credentials (username and password) against a storage system, such as a database. In Basic Authentication:
- The client must send a username and password with every request in the HTTP request header.
- These credentials are combined into a single string in the format “Username:Password“, then encoded in Base64.
- The header is prefixed with the word Basic (e.g., Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l).
Note: While the credentials are encoded in Base64, Base64 is not encryption. Anyone who intercepts the header can decode it easily. Therefore, Basic Authentication should only be used over HTTPS to avoid sending credentials in plain text (encoded in base64) over the network.
How does Basic Authentication Work in ASP.NET Core Web API?
For a better understanding of how the Basic Authentication Works in ASP.NET Core Web API, please have a look at the following diagram.
Basic Authentication is a straightforward authentication mechanism where the client sends a username and password with each request. These credentials are typically sent in the HTTP Authorization header, encoded as a Base64 string. Let us understand how Basic Authentication works in ASP.NET Core Web API step by step:
- Credentials Encoding: The client combines the username and password into a string of the format username:password. For example, if the username is admin and the password is password123, the combined string would be admin:password123.
- Base64 Encoding: The username:password string is then encoded into Base64. For example, admin:password123 becomes YWRtaW46cGFzc3dvcmQxMjM=. This encoding is not encryption, and it’s just a way to represent the credentials in a safe-to-transmit format.
- Authorization Header: The Base64-encoded string is included in the request’s Authorization header, prefixed with the word Basic. For Example: Authorization: Basic YWRtaW46cGFzc3dvcmQxMjM=
- Server Receives the Request: The server checks if an Authorization header is present and whether it begins with Basic. If the header is missing or contains invalid credentials, the server responds with 401 (Unauthorized) and includes a WWW-Authenticate: Basic header to indicate that Basic Authentication is required.
- Credentials Decoding and Verification: If the header is found, the server extracts the base64 encoded credentials and decodes them back into the username:password format. The server then verifies these credentials against a known data source (such as a database, in-memory list, or other storage).
- If the credentials are valid, the server processes the request and returns the appropriate response (e.g., data from a protected endpoint).
- The server responds with 401 (Unauthorized) if the credentials are invalid.
Note: Because the server is stateless, this process happens for each and every request.
Implementing Basic Authentication in ASP.NET Core Web API Application:
Let us implement Basic Authentication in an ASP.NET Core Web API Application. We will do the following things to implement and understand the Basic Authentication in ASP.NET Core Web API Application:
- Server Application: We will create an ASP.NET Core Web API Project that will act as the server application. This application will validate the user with Basic Authentication when accessing the resources which need authentication.
- Create the User Model: This model will represent the user data.
- Create the User Repository: This repository will handle the CRUD Operations and also a method to validate users.
- Setup Basic Authentication: We will configure the authentication scheme in the application.
- Create an API Controller: This controller will handle CRUD operations for the user model using the User Repository.
- Create a Client Application: We will create a Dot Net Console Application as the Client Application that will consume the Web API services with Basic Authentication.
Creating Server Application
Create a new ASP.NET Core Web API Project with the name BasicAuthenticationDemo. This will be our Server application, i.e., we will develop restful services that the client application will consume. Once you create the project, please install the Entity Framework Core packages by executing the following commands in the Package Manager Console:
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
Create the User Model:
Within the Project root directory, create a folder named Models. Next, create a class file named User.cs within the Models folder and copy and paste the following code. This will be the model which will hold the user data.
using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; namespace BasicAuthenticationDemo.Models { [Index(nameof(Email), Name = "Index_Email_Unique", IsUnique = true)] public class User { [Key] public int Id { get; set; } [Required] [StringLength(50)] public string FirstName { get; set; } [Required] [StringLength(50)] public string LastName { get; set; } [Required] [EmailAddress] [StringLength(100)] public string Email { get; set; } [Required] [MinLength(6)] public string Password { get; set; } } }
UserDbContext:
Create a class file named UserDbContext.cs in the Models folder, then copy and paste the following code.
using Microsoft.EntityFrameworkCore; namespace BasicAuthenticationDemo.Models { public class UserDbContext : DbContext { public UserDbContext(DbContextOptions<UserDbContext> options) : base(options) { } public DbSet<User> Users { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<User>().HasData( new User { Id = 1, FirstName = "John", LastName = "Doe", Email = "john.doe@example.com", Password = "password123" }, new User { Id = 2, FirstName = "Jane", LastName = "Smith", Email = "jane.smith@example.com", Password = "password123" } ); } } }
UserDTO (Data Transfer Object)
This DTO will transfer data in/out of the API instead of exposing your entity directly. It is often best practice to separate our entity model from what we expose externally. So, create a class file named UserDTO.cs within the Models folder and then copy and paste the following code
using System.ComponentModel.DataAnnotations; namespace BasicAuthenticationDemo.DTOs { public class UserDTO { public int Id { get; set; } [Required] [StringLength(50, ErrorMessage = "First name cannot exceed 50 characters")] public string FirstName { get; set; } [Required] [StringLength(50, ErrorMessage = "Last name cannot exceed 50 characters")] public string LastName { get; set; } [Required] [EmailAddress] [StringLength(100, ErrorMessage = "Email cannot exceed 100 characters")] public string Email { get; set; } [Required] [MinLength(6, ErrorMessage = "Password must be at least 6 characters long")] public string Password { get; set; } } }
Create the User Service:
Next, create a class file named UserService.cs within the Models folder and copy and paste the following code. This class will perform the typical database CRUD Operations on the User entity. The following code is self-explained, so please read the comment lines for a better understanding:
using Microsoft.EntityFrameworkCore; namespace BasicAuthenticationDemo.Models { public interface IUserService { Task<IEnumerable<User>> GetUsersAsync(); Task<User?> GetUserByIdAsync(int id); Task<User> CreateUserAsync(User user); Task<bool> UpdateUserAsync(User user); Task<bool> DeleteUserAsync(int id); Task<User?> ValidateUserAsync(string email, string password); } public class UserService : IUserService { private readonly UserDbContext _context; public UserService(UserDbContext context) { _context = context; } public async Task<IEnumerable<User>> GetUsersAsync() { return await _context.Users.AsNoTracking().ToListAsync(); } public async Task<User?> GetUserByIdAsync(int id) { return await _context.Users.FindAsync(id); } public async Task<User> CreateUserAsync(User user) { _context.Users.Add(user); await _context.SaveChangesAsync(); return user; } public async Task<bool> UpdateUserAsync(User user) { var existingUser = await _context.Users.FindAsync(user.Id); if (existingUser == null) { return false; } existingUser.FirstName = user.FirstName; existingUser.LastName = user.LastName; existingUser.Email = user.Email; existingUser.Password = user.Password; await _context.SaveChangesAsync(); return true; } public async Task<bool> DeleteUserAsync(int id) { var user = await _context.Users.FindAsync(id); if (user == null) return false; _context.Users.Remove(user); await _context.SaveChangesAsync(); return true; } public async Task<User?> ValidateUserAsync(string email, string password) { // In real-world scenarios, you would compare hashed password to a hashed version in DB. return await _context.Users .FirstOrDefaultAsync(u => u.Email == email && u.Password == password); } } }
Note: The ValidateUserAsync is the method using which we check if the email and password match a record in the database. For production apps, never store raw passwords. Use hashed passwords + salt and compare hashes during validation.
Create a Custom Authentication Handler
In ASP.NET Core, we can create a custom AuthenticationHandler<T> to handle Basic Auth. So, create a class file named BasicAuthenticationHandler.cs within the Models folder and then copy and paste the following code: The following code is self-explained, so please read the comment lines for a better understanding:
using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; namespace BasicAuthenticationDemo.Models { // A custom authentication handler that derives from AuthenticationHandler<TOptions>. // The generic type TOptions is the type of authentication options that the handler works with. public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { // A reference to the user service, which is used to validate user credentials. private readonly IUserService _userService; // Constructor for BasicAuthenticationHandler. // parameters: // - IOptionsMonitor<AuthenticationSchemeOptions> options: Monitors changes to authentication scheme options // - ILoggerFactory logger: Factory to create logger instances // - UrlEncoder encoder: Encodes URLs to ensure they are safe // - IUserService userService: The service used to validate user credentials. public BasicAuthenticationHandler( IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, IUserService userService) //Passes options, logger, and encoder to the base class constructor : base(options, logger, encoder) { // Assign the provided IUserService to the private field for use in authentication logic. _userService = userService; } // The main method responsible for handling authentication. // This method is overridden from AuthenticationHandler<TOptions> and called when a request needs to be authenticated. protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { // Check if the Authorization header is present in the request. if (!Request.Headers.ContainsKey("Authorization")) { // If the Authorization header is missing, fail the authentication with an appropriate message. return AuthenticateResult.Fail("Missing Authorization Header"); } // Retrieve the value of the Authorization header. var authorizationHeader = Request.Headers["Authorization"].ToString(); // Attempt to parse the Authorization header into a structured AuthenticationHeaderValue object. if (!AuthenticationHeaderValue.TryParse(authorizationHeader, out var headerValue)) { // If parsing fails, the header is considered invalid and authentication fails. return AuthenticateResult.Fail("Invalid Authorization Header"); } // Verify that the authorization scheme is "Basic". if (!"Basic".Equals(headerValue.Scheme, StringComparison.OrdinalIgnoreCase)) { // If the scheme is not "Basic", fail authentication with a relevant message. return AuthenticateResult.Fail("Invalid Authorization Scheme"); } // Decode the Base64-encoded credentials from the authorization header parameter. // This yields a "username:password" string which is then split by the colon. var credentials = Encoding.UTF8.GetString(Convert.FromBase64String(headerValue.Parameter)).Split(':', 2); // Check if splitting the credentials results in exactly two components (username and password). if (credentials.Length != 2) { // If not, the credentials are invalid and authentication fails. return AuthenticateResult.Fail("Invalid Basic Authentication Credentials"); } // Extract the email (username) and password from the decoded credentials. var email = credentials[0]; var password = credentials[1]; try { // Use the IUserService to validate the user credentials. var user = await _userService.ValidateUserAsync(email, password); if (user == null) { // If no user matches the provided credentials, fail authentication. return AuthenticateResult.Fail("Invalid Username or Password"); } // If the credentials are valid, create claims for the user. // Claims describe the user (ID, email, roles, etc.). var claims = new[] { // A unique identifier for the user. new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), // The user's email, stored as their "name" claim. new Claim(ClaimTypes.Name, user.Email) }; // Create a ClaimsIdentity with the specified claims and authentication scheme // ClaimsIdentity groups those claims and specifies an authentication type, // indicating a single identity the user has. var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name); // Create a ClaimsPrincipal based on the ClaimsIdentity // ClaimsPrincipal is the container that can hold one or more ClaimsIdentity objects // enabling multiple ways a user might be authenticated. var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); // AuthenticationTicket is the object used by ASP.NET Core to store and // track the authenticated user’s ClaimsPrincipal during an authentication session. var authenticationTicket = new AuthenticationTicket(claimsPrincipal, Scheme.Name); // Indicate that authentication was successful and return the ticket return AuthenticateResult.Success(authenticationTicket); } catch { // If any exception occurs during authentication, fail with a generic error message. return AuthenticateResult.Fail("Error occurred during authentication"); } } } }
Understanding Claims, ClaimsIdentity, ClaimsPrincipal, AuthenticationTicket:
In ASP.NET Core (and .NET in general), these four concepts (Claims, ClaimsIdentity, ClaimsPrincipal, and AuthenticationTicket) work together to represent a user’s identity and manage authentication/authorization within your application. Let us understand these objects in detail and try to understand the Roles and Responsibilities of these objects in ASP.NET Core Web API Authentication and Authorization:
Claims
A claim is a statement about a user. It typically includes data such as a user ID, username, email, role, or other attributes (e.g., ClaimTypes.Name, ClaimTypes.Email, ClaimTypes.Role, etc.). These claims collectively describe who the user is.
Claims allow us to store and pass user-related data throughout the system without repeatedly fetching them from a data source. Each claim consists of a type (like “Name”) and a value (like “John Doe”). The type is a category, while the value is the actual data. For example, if a user logs in with the email alice@example.com and has an internal ID of 123, you might create two claims:
-
- ClaimTypes.NameIdentifier → “123”
- ClaimTypes.Email → “alice@example.com”
Code used in our Example:
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Email, user.Email) };
ClaimsIdentity
A ClaimsIdentity object represents a single authenticated identity. Internally, it stores a set of claims plus information about how the user was authenticated (the AuthenticationType).
A ClaimsIdentity is effectively the container for a user’s claims. It also tracks whether the identity is authenticated and which mechanism (e.g., “Basic”, “Cookies”, “Bearer”, etc.) performed the authentication.
When a user logs in successfully, the system creates a ClaimsIdentity with the user’s claims. The AuthenticationType might be set to “Basic” for Basic Authentication or “Cookies” for cookie-based logins, or “Bearer” for token-based authentication.
Code used in our Example:
var identity = new ClaimsIdentity(claims, “Basic”);
The second parameter is the ‘Scheme.Name’ or authentication type, e.g., “Basic.” Now, you have a single identity containing the user’s claims and knowledge that they were authenticated by “Basic” auth.
ClaimsPrincipal
A ClaimsPrincipal is the top-level security principal in .NET that can contain one or more ClaimsIdentity objects. In many applications, users might have multiple identities. For example, one identity could be from a corporate login (e.g., Windows authentication) and another from a custom token-based system. A ClaimsPrincipal allows these multiple identities to coexist. This is especially relevant for scenarios involving multi-factor authentication.
When a user is authenticated (e.g., with Basic Authentication), you typically create a single ClaimsIdentity and wrap it in a ClaimsPrincipal. Then, the ASP.NET Core context (HttpContext.User) references this ClaimsPrincipal, making it easy to check claims or identity details anywhere in your application.
Code in our Example:
var principal = new ClaimsPrincipal(identity);
This principal now holds the single Basic Auth identity. If you had multiple identities, you need to add them all to the ClaimsPrincipal.
AuthenticationTicket
When authentication is processed, the framework often expects an AuthenticationTicket to encapsulate the user’s ClaimsPrincipal and the name of the authentication scheme. This ticket is what ASP.NET Core uses internally for things like persisting sign-in details, generating cookies or tokens, and verifying that a user is still valid on subsequent requests.
In many authentication handlers (e.g., Basic, JWT, OAuth), the handler creates a new AuthenticationTicket with the user’s ClaimsPrincipal and the relevant scheme name once the user is validated. ASP.NET Core can say, “User is authenticated via Scheme X, here’s the principal with claims,” and store or forward that AuthenticationTicket as needed.
Code in our Example:
var ticket = new AuthenticationTicket(principal, "Basic"); return AuthenticateResult.Success(ticket);
Here, the successful result includes an AuthenticationTicket that ties the user’s ClaimsPrincipal to the “Basic” auth scheme.
Modify appsettings.json
Make sure to add a connection string to your appsettings.json file. So, please modify the appsettings.json file as follows.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=UsersDB;Trusted_Connection=True;TrustServerCertificate=True;" } }
Register Basic Authentication and Services in Program Class
We need to configure the Authentication Scheme in the Program class as Basic. So, please modify the Program class as follows.
using BasicAuthenticationDemo.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; namespace BasicAuthenticationDemo { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. //1. Ignore Camel Case with JSON Serialization builder.Services.AddControllers() .AddJsonOptions(options => { // This will use the property names as defined in the C# model options.JsonSerializerOptions.PropertyNamingPolicy = null; }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // 2. Add DbContext builder.Services.AddDbContext<UserDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection"))); // 3. Register the UserService builder.Services.AddScoped<IUserService, UserService>(); // 4. Add Authentication with our BasicAuthenticationHandler builder.Services.AddAuthentication("BasicAuthentication") .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } } }
Understanding the Code:
AddAuthentication(“BasicAuthentication”):
This line configures the default authentication scheme for the application. Here, “BasicAuthentication” is both a scheme name and a default scheme. When the ASP.NET Core authentication system tries to authenticate a request, and no other scheme is specified, it will use “BasicAuthentication” as the fallback.
.AddScheme():
This method registers a custom authentication scheme within ASP.NET Core’s authentication system.
- AuthenticationSchemeOptions: The type of configuration/option class used by this scheme (in many simple cases, the default AuthenticationSchemeOptions is sufficient).
- BasicAuthenticationHandler: The custom handler class that will handle the authentication logic for this scheme. This is where we parse the Authorization header, decode credentials, validate the user, etc.
- “BasicAuthentication” (again) is the name of the scheme. This name is crucial because it must match the string we specify in [Authorize(AuthenticationSchemes = “BasicAuthentication”)] when we want to protect an endpoint with Basic Authentication specifically.
- The final parameter, null, is an optional display name for the scheme (used sometimes for UI or debugging). If it’s not needed, null is common.
API Controller for CRUD Operations
Next, create an API Empty Controller named UsersController within the Controller folder and copy and paste the following code. The following controller shows how to consume the UserService for CRUD operations. We also apply the [Authorize(AuthenticationSchemes = “BasicAuthentication”)] attribute to protect the endpoints with Basic Authentication. The following code is self-explained, so please read the comment lines for a better understanding:
using BasicAuthenticationDemo.DTOs; using BasicAuthenticationDemo.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace BasicAuthenticationDemo.Controllers { [ApiController] [Route("api/[controller]")] [Authorize(AuthenticationSchemes = "BasicAuthentication")] public class UserController : ControllerBase { private readonly IUserService _userService; public UserController(IUserService userService) { _userService = userService; } // GET: api/User [HttpGet] public async Task<ActionResult<IEnumerable<UserDTO>>> Get() { var users = await _userService.GetUsersAsync(); var userDtos = users.Select(u => new UserDTO { Id = u.Id, FirstName = u.FirstName, LastName = u.LastName, Email = u.Email, Password = u.Password }).ToList(); return Ok(userDtos); } // GET: api/User/1 [HttpGet("{id}")] public async Task<ActionResult<UserDTO>> Get(int id) { var user = await _userService.GetUserByIdAsync(id); if (user == null) return NotFound(); var userDto = new UserDTO { Id = user.Id, FirstName = user.FirstName, LastName = user.LastName, Email = user.Email, Password = user.Password }; return Ok(userDto); } // POST: api/User [HttpPost] public async Task<ActionResult<UserDTO>> Create([FromBody] UserDTO userDto) { if (!ModelState.IsValid) return BadRequest(ModelState); // Map DTO -> Entity var user = new User { FirstName = userDto.FirstName, LastName = userDto.LastName, Email = userDto.Email, Password = userDto.Password }; user = await _userService.CreateUserAsync(user); // Map Entity -> DTO userDto.Id = user.Id; return CreatedAtAction(nameof(Get), new { id = user.Id }, userDto); } // PUT: api/User/1 [HttpPut("{id}")] public async Task<IActionResult> Update(int id, [FromBody] UserDTO userDto) { if (!ModelState.IsValid) return BadRequest(ModelState); if (id != userDto.Id) return BadRequest("ID in URL doesn't match ID in payload."); // Map DTO -> Entity var user = new User { Id = userDto.Id, FirstName = userDto.FirstName, LastName = userDto.LastName, Email = userDto.Email, Password = userDto.Password }; var updated = await _userService.UpdateUserAsync(user); if (!updated) return NotFound(); return NoContent(); } // DELETE: api/User/1 [HttpDelete("{id}")] public async Task<IActionResult> Delete(int id) { var deleted = await _userService.DeleteUserAsync(id); if (!deleted) return NotFound(); return NoContent(); } } }
Database Migration
Next, we need to generate the Migration and update the database schema. So, open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows.
With this. our Database with Users tables should be created as shown in the image below:
Creating Client Application:
Let us create a .NET Console Application that consumes the ASP.NET Core Web API endpoints secured with Basic Authentication. This example will demonstrate how to:
- Send Basic Auth credentials (username + password) in the Authorization header.
- Perform CRUD operations (GET, POST, PUT, DELETE) on the User resource.
- Serialize and Deserialize JSON data using System.Text.Json.
First, create a Console application named BasicAuthConsoleClient. Once you create the Console Application, modify the Program class as follows. The following code demonstrates a typical use case of HttpClient to interact with a REST API, including full CRUD operations and handling of HTTP request headers for Basic Authentication.
using System.Net.Http.Headers; using System.Text; using System.Text.Json; namespace BasicAuthConsoleClient { // A simplified DTO mirroring the UserDTO in your Web API public class UserDTO { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public string Password { get; set; } } public class Program { // Change baseUrl to match your ASP.NET Core Web API address/port. private static readonly string baseUrl = "https://localhost:7187"; // Replace with valid credentials for Basic Auth private static readonly string username = "john.doe@example.com"; private static readonly string password = "password123"; private static async Task Main(string[] args) { // Create and configure HttpClient using var httpClient = new HttpClient(); // Attach Basic Authentication header var authToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authToken); Console.WriteLine("Basic Auth Console Client"); // 1. GET all users await GetAllUsersAsync(httpClient); // 2. GET a single user by Id (e.g., Id = 1) await GetUserByIdAsync(httpClient, 1); // 3. CREATE (POST) a new user var newUserId = await CreateUserAsync(httpClient); // 4. UPDATE (PUT) the new user await UpdateUserAsync(httpClient, newUserId); // 5. DELETE the user await DeleteUserAsync(httpClient, newUserId); Console.WriteLine("All Operations are Completed"); Console.ReadKey(); } private static async Task GetAllUsersAsync(HttpClient httpClient) { Console.WriteLine("\n--- GET ALL USERS ---"); try { var response = await httpClient.GetAsync($"{baseUrl}/api/User"); response.EnsureSuccessStatusCode(); var responseBody = await response.Content.ReadAsStringAsync(); var users = JsonSerializer.Deserialize<List<UserDTO>>(responseBody); Console.WriteLine("Users:"); if (users != null) { foreach (var user in users) { Console.WriteLine($"Id: {user.Id}, Email: {user.Email}, Name: {user.FirstName} {user.LastName}"); } } } catch (Exception ex) { Console.WriteLine($"Error fetching users: {ex.Message}"); } } private static async Task GetUserByIdAsync(HttpClient httpClient, int id) { Console.WriteLine($"\n--- GET USER BY ID: {id} ---"); try { var response = await httpClient.GetAsync($"{baseUrl}/api/User/{id}"); if (!response.IsSuccessStatusCode) { Console.WriteLine($"Failed to get user. HTTP Status: {response.StatusCode}"); return; } var responseBody = await response.Content.ReadAsStringAsync(); var user = JsonSerializer.Deserialize<UserDTO>(responseBody); if (user != null) { Console.WriteLine($"User found - Id: {user.Id}, Email: {user.Email}, Name: {user.FirstName} {user.LastName}"); } } catch (Exception ex) { Console.WriteLine($"Error fetching user by Id: {ex.Message}"); } } private static async Task<int> CreateUserAsync(HttpClient httpClient) { Console.WriteLine("\n--- CREATE NEW USER (POST) ---"); var newUser = new UserDTO { FirstName = "Jane", LastName = "Smith", Email = $"jane.smith_{Guid.NewGuid()}@example.com", Password = "MyTest123" }; try { var payload = JsonSerializer.Serialize(newUser); var content = new StringContent(payload, Encoding.UTF8, "application/json"); var response = await httpClient.PostAsync($"{baseUrl}/api/User", content); if (!response.IsSuccessStatusCode) { Console.WriteLine($"Failed to create user. HTTP Status: {response.StatusCode}"); return 0; } var responseBody = await response.Content.ReadAsStringAsync(); var createdUser = JsonSerializer.Deserialize<UserDTO>(responseBody); if (createdUser != null) { Console.WriteLine($"Created user Id: {createdUser.Id}, Email: {createdUser.Email}"); return createdUser.Id; } } catch (Exception ex) { Console.WriteLine($"Error creating user: {ex.Message}"); } return 0; } private static async Task UpdateUserAsync(HttpClient httpClient, int userId) { if (userId <= 0) return; Console.WriteLine("\n--- UPDATE USER (PUT) ---"); var updatedUser = new UserDTO { Id = userId, FirstName = "Jane (updated)", LastName = "Smith (updated)", Email = $"jane.smith.updated_{Guid.NewGuid()}@example.com", Password = "MyUpdatedPassword123" }; try { var payload = JsonSerializer.Serialize(updatedUser); var content = new StringContent(payload, Encoding.UTF8, "application/json"); var response = await httpClient.PutAsync($"{baseUrl}/api/User/{userId}", content); if (!response.IsSuccessStatusCode) { Console.WriteLine($"Failed to update user. HTTP Status: {response.StatusCode}"); return; } Console.WriteLine("User successfully updated."); } catch (Exception ex) { Console.WriteLine($"Error updating user: {ex.Message}"); } } private static async Task DeleteUserAsync(HttpClient httpClient, int userId) { if (userId <= 0) return; Console.WriteLine("\n--- DELETE USER ---"); try { var response = await httpClient.DeleteAsync($"{baseUrl}/api/User/{userId}"); if (!response.IsSuccessStatusCode) { Console.WriteLine($"Failed to delete user. HTTP Status: {response.StatusCode}"); return; } Console.WriteLine($"User with Id: {userId} deleted successfully."); } catch (Exception ex) { Console.WriteLine($"Error deleting user: {ex.Message}"); } } } }
Understanding the Code:
- HttpClient is used to send HTTP requests and receive HTTP responses from a resource identified by a URI.
- The example uses HTTP Methods such as GET to fetch data, POST to create data, PUT to update data, and DELETE to remove data.
- Basic authentication is used here, where the username and password are encoded and sent in the header.
- After each call, it checks if the operation was successful and prints the result accordingly. If it is not successful, it prints the error message.
Now, run the application, and you should see the following result:
Generating the Base64 Encode String Online:
It is also possible to generate the Base64 Encode String Online, visit the following URL.
Enter the username and password separated by a colon (:) in the Encode to Base64 format textbox, and then click on the Encode button to generate the Base64 encoded value.
Advantages and Disadvantages of Using Basic Authentication in ASP.NET Core Web API
The following are some of the advantages and disadvantages of using Basic Authentication in ASP.NET Core Web API:
Advantages of Basic Authentication
- Simplicity: It’s straightforward to implement and understand. Requires minimal configuration.
- Widely Supported: Almost all HTTP clients (e.g., Postman, browsers, cURL) can easily send a Basic authorization header.
- Stateless: Works nicely with REST constraints. No sessions or cookies are required on the server side.
- Quick for Prototyping: Very handy in simple or internal scenarios where complex token-based solutions (OAuth, JWT) might be overkill.
Disadvantages of Basic Authentication
- Security Concern (Credentials in Every Request): The username and password are sent in each request. While Base64-encoded, it is not secure by itself. HTTPS is mandatory to prevent credentials from being exposed. Base64 is easily decoded, so the actual credentials are exposed if the connection is not encrypted.
- Lack of Token Revocation: With tokens (like JWT), we can implement token expiration or blacklisting. On the other hand, basic authentication always relies on the same static credentials unless manually changed.
- Poor Scalability: Sending credentials with every request can lead to a higher server load due to frequent authentication checks.
- No Logout Mechanism: Credentials are cached by the client (e.g., browser) and cannot be invalidated easily.
When Should We Use Basic Authentication in Real-Time Applications?
We can consider using Basic Authentication in the following scenarios:
- Simple Internal Services: For small internal APIs or microservices where the overhead of more robust protocols (OAuth, OpenID Connect) is not justifiable, Basic Auth may be acceptable—provided you always use SSL/TLS.
- Low-Risk Environments: If the transmitted data is not highly sensitive (or is still encrypted with TLS), and you do not need advanced features like token revocation or delegated permissions, Basic Auth might suffice.
- Backward Compatibility: Some legacy systems or third-party clients only support Basic Authentication, so you might need it for compatibility.
Basic Authentication is a simple authentication method suitable for specific use cases. However, it is not recommended for highly secure applications. Using HTTPS with Basic Authentication is essential to protect credentials during transmission. Consider using token-based authentication methods like JWT or OAuth2 for more complex scenarios. These methods provide better security, including revoking tokens, more granular permissions (scopes), and less frequent exposure of user credentials.
In the next article, I will discuss how to implement Role-Based Basic Authentication in ASP.NET Core Web API Application. In this article, I explain the ASP.NET Core Web API Basic Authentication step by step with an example. I hope you enjoy this ASP.NET Core Web API Basic Authentication article.
Thank you, overall a good tutorial except for some issues:
The repository is inside the namespace BasicAuthenticationDemo.Models twice.
This results in the other classes having to have the following using statement:
using BasicAuthenticationDemo.Models.BasicAuthenticationDemo.Models;
Is this intended?
Is it required by ASP.NET Core Web API?
Also the portnumbers of the API and the console application are different which results in them not connecting properly.
After setting the same port on both applications they still do not connect because the console application throws the following exception:
System.Net.Http.HttpRequestException
HResult=0x80131501
Message=The SSL connection could not be established, see inner exception.
Source=System.Net.Http
StackTrace:
at System.Net.Http.ConnectHelper.d__2.MoveNext()
at System.Threading.Tasks.ValueTask`1.get_Result()
at System.Net.Http.HttpConnectionPool.d__103.MoveNext()
at System.Threading.Tasks.ValueTask`1.get_Result()
at System.Net.Http.HttpConnectionPool.d__105.MoveNext()
at System.Threading.Tasks.ValueTask`1.get_Result()
at System.Net.Http.HttpConnectionPool.d__79.MoveNext()
at System.Threading.Tasks.TaskCompletionSourceWithCancellation`1.d__1.MoveNext()
at System.Threading.Tasks.ValueTask`1.get_Result()
at System.Net.Http.HttpConnectionPool.d__89.MoveNext()
at System.Threading.Tasks.ValueTask`1.get_Result()
at System.Net.Http.RedirectHandler.d__4.MoveNext()
at System.Net.Http.HttpClient.<g__Core|83_0>d.MoveNext()
at ApiConsumerApp.Program.d__2.MoveNext() in C:\FKApps\BasicAuthenticationDemo\APIConsumerApp\Program.cs:line 64
at ApiConsumerApp.Program.d__1.MoveNext() in C:\FKApps\BasicAuthenticationDemo\APIConsumerApp\Program.cs:line 31
This exception was originally thrown at this call stack:
[External Code]
Inner Exception 1:
AuthenticationException: Cannot determine the frame size or a corrupted frame was received.