Recently, I undertook the development of a small Single Sign-On (SSO) portal—a web application designed to provide seamless access to various tools offered by the company. A key requirement for this portal was to allow users to authenticate using either LDAP or credentials stored in the database, ensuring flexibility in scenarios where the LDAP server might be unavailable.
Let’s explore the critical considerations and strategies I employed while managing this project.
Encryption(Hashing)
Passwords should never be stored in plain text. In the event of a database breach, securely storing credentials is essential to safeguard user data.
The first step is to securely store passwords in the database. During login, the system hashes the user-provided password and compares it with the stored hash to verify authentication.
To accomplish this, a hashing algorithm is used. Hashing is a one-way process, meaning that it’s impossible to reverse a hash back to the original text. This property is critical, as it prevents attackers from recalculating passwords even if they gain access to the database. The result of hashing is a unique “fingerprint” that represents the password securely.
Hashing algorithm
While there are many hashing algorithms available today, not all are equally suitable for every scenario. In my application, I introduced a configuration key to specify the hashing algorithm, allowing flexibility to switch algorithms in the future. This approach also supports a gradual migration from older algorithms to newer, more secure options.
One widely used algorithm is bCrypt, valued for its intentional slowness, which effectively mitigates brute-force attacks by significantly increasing the time required to compute hashes.
Salting
While hashing passwords is crucial, it’s not sufficient on its own. If an attacker gains access to the database and uses a precomputed list of text-to-hash mappings, known as a rainbow table, they could deduce users’ passwords. For example, the hash of ‘Pippo111’ (using SHA256) will always be: 426ecb09f1c4fc39d4f66d416c6356cb182dc6247bb3d59341ab3b054c1cc7bb
To mitigate this, a random string called salt is added to the password before hashing. This ensures that even if two users have the same password, their resulting hashes will be unique, rendering rainbow table attacks far less effective.
The salt is stored in the database because it is required by the application to generate and verify hashes.
In my application, I enhanced security further by recalculating the salt every time a user changed or reset their password.
HTTPS
The connection to the SSO portal was configured to use HTTPS exclusively—a best practice today but still overlooked in some smaller organizations. HTTPS ensures that no one can intercept the payload during login, protecting user credentials from being read.
Parameterized queries
Another critical aspect is how queries are executed in the application. In my case, the application was developed using .NET Core with Entity Framework to handle database queries. However, for custom queries, it’s crucial to use parameterized queries to prevent SQL injection attacks.
Here’s an example:
Incorrect approach:
using (SqlCommand cmd = new SqlCommand("", conn))
{
cmd.CommandText = @"
Select
username,
salt,
algorithmType,
password
from T_Credentials
where username = '"+ varUsername +"'";
using (SqlDataReader reader = cmd.ExecuteReader())
{
//...
}
}
CORREct approach:
using (SqlCommand cmd = new SqlCommand("", conn))
{
cmd.CommandText = @"
Select
username,
salt,
algorithmType,
password
from T_Credentials
where username = @username";
cmd.Parameters.AddWithValue("@username", varUsername);
using (SqlDataReader reader = cmd.ExecuteReader())
{
//...
}
}
By following this approach, you can prevent SQL injection attacks. In addition, my application also validated input so that the username
variable was already sanitized before being used in the query.
Blocking after multiple failed attempts
To prevent brute-force attacks, I implemented two levels of security: one at the application level and another at the user level.
- Application Level: Monitors incoming requests from a specific client. If an abnormal number of requests is detected, the system can automatically block the client for a set period (e.g., X minutes).
- User Level: Locks the user account after five failed login attempts (a configurable parameter). In the credentials table, I added a field to track the number of failed attempts and another to indicate the user account status (active, disabled, suspended, expired, etc.).
The failed login counter resets upon a successful login.
Conclusions
In this article, we explored key strategies for securely managing credentials stored in a database. The essential points to keep in mind include:
- Never store passwords in plain text.
- Always hash passwords with a unique random string (salt) added before hashing.
- Use a slow hashing algorithm to increase resistance to brute-force attacks.
- Always enforce HTTPS to secure communication.
- Use parameterized queries to protect against SQL injection attacks.
- Implement blocking mechanisms to prevent brute-force attacks.
From an application perspective, the workflow follows these steps:
By following these best practices, you can significantly enhance the security of your authentication systems and better protect user credentials.
Bye bye!