This it is part of assignment submitted to Deakin University, School of IT, Unit SIT331 - Full Stack Development Secure Backend Services. By s222575621.
Objective
The objective of this teaching case is to provide learners with the knowledge and skills to implement a basic authentication and authorization layer in an ASP.NET Core Web API application. This task includes creating the necessary infrastructure for user management, implementing a basic authentication handler, and setting up authorization policies to control access to various API endpoints.
Case SummaryThis teaching case guides learners through the process of adding authentication and authorization to a Web API application. The learners will create new user management functionalities, implement a basic authentication handler, and enforce authorization policies. Each part of the implementation is explained with detailed comments and step-by-step instructions to ensure clarity and understanding.
Basic Concepts and StructureIntroduction to Authentication and Authorization:
Authentication verifies the identity of a user or service, while authorization determines their access rights within the system. In the context of ASP.NET Core Web API, these processes are managed through middleware and various services provided by the framework.
Bounded Context:
A bounded context is a logical boundary within which a particular model is defined and applicable. In this case, we will create a User
bounded context to
manage user-related data and operations, such as registration, login, and user information retrieval.
Infrastructure SetupProject Structure:
Controllers Folder:
- UserController.cs: Manages user-related API endpoints.
- RobotCommandController.cs: Manages robot command endpoints.
- MapsController.cs: Manages map-related endpoint
Models Folder:
- User.cs: Defines the structure of the user entity.
- Login.cs: Defines the structure for login data.
Persistence Folder:
- UserDataAccess.cs: Handles data access operations for user entities.
Authentication Folder:
- BasicAuthenticationHandler.cs: Implements the basic authentication logic.
Step 1: Setting Up User Management
1.UserModel.cs:
public class User
{
public int Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string PasswordHash { get; set; }
public string Description { get; set; }
public string Role { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime ModifiedDate { get; set; }
}
Explanation:
This model defines the properties of a user in the system. The PasswordHash property stores the hashed password for security reasons. The Role property is used to assign roles like "Admin" or "User" for authorization purposes.
2. Login.cs:
public class Login
{
public string Email { get; set; }
public string Password { get; set; }
}
Explanation:
This model captures the login information provided by the user. It includes the Email and Password properties, which are essential for authentication.
3. UserDataAccess.cs:
public static User GetUserByEmail(string email)
{
using var conn = new NpgsqlConnection(CONNECTION_STRING);
conn.Open();
const string query = "SELECT * FROM \"user\" WHERE \"Email\" = @Email";
using var cmd = new NpgsqlCommand(query, conn);
cmd.Parameters.AddWithValue("@Email", email);
using var dr = cmd.ExecuteReader();
if (dr.Read())
{
return new User
{
Id = dr.GetInt32(0),
Email = dr.GetString(1),
FirstName = dr.GetString(2),
LastName = dr.GetString(3),
PasswordHash = dr.GetString(4),
Description = dr.IsDBNull(5) ? null : dr.GetString(5),
Role = dr.IsDBNull(6) ? "User" : dr.GetString(6),
CreatedDate = dr.GetDateTime(7),
ModifiedDate = dr.GetDateTime(8)
};
}
return null;
}
Explanation:
1. Database Connection:
- 'using var conn = new NpgsqlConnection(CONNECTION_STRING);': Establishes a connection to the PostgreSQL database using the connection string defined in 'CONNECTION_STRING'.
- 'conn.Open();': Opens the connection to the database.
2. SQL Query:
- 'const string query = "SELECT * FROM \"user\" WHERE \"Email\" = @Email";': Defines an SQL query to select a user record based on the provided email.
- 'using var cmd = new NpgsqlCommand(query, conn);': Creates a command object to execute the query.
- 'cmd.Parameters.AddWithValue("@Email", email);': Adds the email parameter to the command to prevent SQL injection.
3. Executing the Query:
- 'using var dr = cmd.ExecuteReader();': Executes the query and returns a data reader to read the results.
- 'if (dr.Read())': Checks if a record was returned.
4. Mapping Data to User Object:
- Creates a new 'User' object and maps the data from the database to the properties of the 'User' object.
- Handles null values appropriately using 'dr.IsDBNull'.
5. Return Value:
- Returns the 'User' object if a user is found.
- Returns 'null' if no user is found.
Step 2: Implementing Basic Authentication Handler
BasicAuthenticationHandler.cs:
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var endpoint = Context.GetEndpoint();
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
{
// Allow anonymous access
return AuthenticateResult.NoResult();
}
Response.Headers.Add("WWW-Authenticate", @"Basic realm=""Access to the robot controller.""");
var authHeader = Request.Headers["Authorization"].ToString();
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic "))
{
return AuthenticateResult.Fail("No credentials provided.");
}
var encodedCredentials = authHeader.Substring("Basic ".Length).Trim();
var credentials = Encoding.UTF8.GetString(Convert.FromBase64String(encodedCredentials));
var email = credentials.Split(':')[0];
var password = credentials.Split(':')[1];
var user = UserDataAccess.GetUserByEmail(email);
if (user == null || !BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
{
return AuthenticateResult.Fail("Authentication failed.");
}
var claims = new[]
{
new Claim(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"),
new Claim(ClaimTypes.Role, user.Role)
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
Explanation:
1. Anonymous Access Check:
- 'var endpoint = Context.GetEndpoint();': Gets the endpoint being accessed.
- 'if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)': Checks if the endpoint allows anonymous access.
- 'return AuthenticateResult.NoResult();': Allows access without authentication if the endpoint is marked as '[AllowAnonymous]'.
2. Setting WWW-Authenticate Header:
- 'Response.Headers.Add("WWW-Authenticate", @"Basic realm=""Access to the robot controller.""");': Adds a header to the response to prompt for basic authentication.
3. Reading Authorization Header:
- 'var authHeader = Request.Headers["Authorization"].ToString();': Reads the 'Authorization' header from the request.
- 'if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic "))': Checks if the header is present and starts with "Basic ".
- 'return AuthenticateResult.Fail("No credentials provided.");': Fails authentication if the header is missing or incorrectly formatted.
4. Decoding Credentials:
- 'var encodedCredentials = authHeader.Substring("Basic ".Length).Trim();': Extracts the Base64-encoded credentials from the header.
- 'var credentials =Encoding.UTF8.GetString(Convert.FromBase64String(encodedCredentials));': Decodes the credentials.
- 'var email = credentials.Split(':')[0];': Extracts the email.
- 'var password = credentials.Split(':')[1];': Extracts the password.
5. User Verification (BCrypt hashing algorithm):
- 'var user = UserDataAccess.GetUserByEmail(email);': Retrieves the user from the database by email.
- 'if (user == null || !BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))': Verifies the password using BCrypt.
- 'return AuthenticateResult.Fail("Authentication failed.");': Fails authentication if the user is not found or the password is incorrect.
6. Creating Claims:
- 'var claims = new[] { new Claim(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"), new Claim(ClaimTypes.Role, user.Role) };': Creates claims for the user's name and role.
7. Creating Authentication Ticket:
- 'var identity = new ClaimsIdentity(claims, Scheme.Name);': Creates a claims identity.
- 'var principal = new ClaimsPrincipal(identity);': Creates a claims principal.
- 'var ticket = new AuthenticationTicket(principal, Scheme.Name);': Creates an authentication ticket.
8. Returning Success:
- 'return AuthenticateResult.Success(ticket);': Returns a successful authentication result.
builder.Services.AddAuthentication("BasicAuthentication").AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireClaim(ClaimTypes.Role, "Admin"));
options.AddPolicy("UserOnly", policy =>
policy.RequireClaim(ClaimTypes.Role, "User", "Admin"));
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Explanation:
Adding Authentication:
builder.Services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
- AddAuthentication: This method adds authentication services to the dependency injection container. The string "BasicAuthentication" is the name of the authentication scheme.
- AddScheme: This method specifies the authentication handler to be used for the "BasicAuthentication" scheme. The 'BasicAuthenticationHandler' is the custom handler that processes authentication requests.
Adding Authorization:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireClaim(ClaimTypes.Role, "Admin"));
options.AddPolicy("UserOnly", policy =>
policy.RequireClaim(ClaimTypes.Role, "User", "Admin"));
});
- AddAuthorization: This method adds authorization services to the dependency injection container.
- AddPolicy: This method defines authorization policies. Each policy specifies a set of requirements that must be met for the policy to be satisfied.
- AdminOnly Policy: Requires the "Role' claim to be "Admin".
- UserOnly Policy: Requires the "Role' claim to be either "User" or "Admin".
Building the Application:
var app = builder.Build();
- This line builds the application, finalizing the configuration.
Using Authentication and Authorization Middlewa
app.UseAuthentication();
app.UseAuthorization();
- UseAuthentication: Adds the authentication middleware to the request pipeline. This middleware handles the authentication of requests.
- UseAuthorization: Adds the authorization middleware to the request pipeline. This middleware handles the authorization of requests, ensuring that authenticated users have the necessary permissions to access resources.
[Authorize(Policy = "AdminOnly")]
[HttpPost]
public ActionResult CreateUser(User user)
{
if (user == null) return BadRequest("User cannot be null");
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(user.PasswordHash);
UserDataAccess.InsertUser(user);
return CreatedAtRoute("UsersRoute", new { id = user.Id }, user);
}
Explanation:
Authorize Attribute:
[Authorize(Policy = "AdminOnly")]
- This attribute specifies that the 'CreateUser' action requires the "AdminOnly" policy to be satisfied. Only users with the "Admin" role can access this endpoint.
HTTP Post Attribute:
[HttpPost]
- This attribute indicates that the 'CreateUser' method responds to HTTP POST requests.
CreateUser Method:
public ActionResult CreateUser(User user)
{
if (user == null) return BadRequest("User cannot be null");
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(user.PasswordHash);
UserDataAccess.InsertUser(user);
return CreatedAtRoute("UsersRoute", new { id = user.Id }, user);
}
Parameter Validation:
if (user == null) return BadRequest("User cannot be null");
- Checks if the 'user' parameter is null. If it is, returns a 'BadRequest' response.
Password Hashing:
user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(user.PasswordHash);
- Hashes the user's password using the BCrypt algorithm before storing it in the database. This ensures that passwords are stored securely.
Inserting User:
UserDataAccess.InsertUser(user);
- Calls the 'InsertUser' method from the 'UserDataAccess' class to insert the new user into the database.
Returning Response:
return CreatedAtRoute("UsersRoute", new { id = user.Id }, user);
- Returns a 'CreatedAtRoute' response, indicating that a new resource has been created. The response includes the route name ("UsersRoute") and the new user's ID.
Code description & testing demonstration video:
ConclusionConfiguring authentication and authorization in 'Program.cs' is a fundamental step to ensure your ASP.NET Core Web API application can securely handle user authentication and enforce access control based on user roles. By integrating the 'BasicAuthenticationHandler', you establish a robust mechanism to validate user credentials, enhancing the application's security posture. The use of BCrypt for password hashing and verification adds an additional layer of security, protecting against common attacks such as brute force and rainbow table attacks.
Implementing authorization policies at the controller level provides fine-grained control over access to specific actions, ensuring that only users with the appropriate roles can perform certain operations. This approach not only secures sensitive endpoints but also simplifies the management of user permissions, making the application more maintainable and scalable.
The detailed walkthrough of the 'UserDataAccess' and 'BasicAuthenticationHandler' classes, combined with practical examples and explanations, helps learners grasp the underlying principles of secure web development. By focusing on the core concepts and avoiding the exposure of complete code blocks, this teaching case encourages learners to understand and implement these security measures themselves. This method aligns with best practices in education, promoting a deeper comprehension and long-term retention of the material, ultimately preparing learners to develop secure and robust web applications.
This it is part of assignment submitted to Deakin University, School of IT, Unit SIT331 - Full Stack Development Secure Backend Services. By s222575621.
Comments
Please log in or sign up to comment.