Password Authentication: How to Correctly Do It
In this article, I would like to explain how to implement storing user authentication data with password authentication.
Join the DZone community and get the full member experience.
Join For FreeThe problem of cybersecurity is quite severe nowadays. Even large and well-known companies face the problem of sensitive user data leakage. It can be unauthorized access to databases, leaked logs, etc. Quite often, we encounter day 0 vulnerabilities that attackers can exploit. All this negatively affects the security of users themselves and the business's reputation. In this article, I would like to explain how to implement storing user authentication data with password authentication.
Authentication
Authentication is the process of confirming by the user that he is the owner of the presented identifier. The most apparent and most familiar authentication process is password authentication. The user goes to the login page, enters his username/password, and logs on. In this article, I will show how you can implement authentication on the server.
The authentication process can be represented as a diagram:
Having received the request, the server checks the user's data with the values stored in the database (which were saved during registration) and gives a verdict on whether the user can be authenticated or not. If the check succeeds, as a rule, a session is created on the server, and its identifier is passed as a cookie in the response.
How can authentication data be saved when a user registers?
Storing Passwords as Plain Text
In this case, the data in the database will be stored as open data. Anyone who has access to the database will be able to get the user's password. This could be a database administrator, a support employee, or a developer. Moreover, there is always the risk of vulnerabilities in the system, which may allow intruders to access the database and download its dump. There are so many services that we use every day. Ideally, each service should have its unique password allowing you to avoid the consequences of leaks of authentication data from any service. But with so many services we use, it's impossible to remember all passwords. One solution is a password manager, but a minority uses thema, and users tend to have one or more passwords that they use everywhere. When data leaks from one service, other services that use the password become compromised, it is strongly recommended not to keep passwords in plain text to protect your users from such problems.
Passwords Hashing
Hashing is the computation of a specific function from a user's password. The function works so that it is possible to get the hash from the password quickly enough, while the reverse conversion cannot be done in adequate time. Examples of hash functions are MD5, SHA-1, SHA-256, etc. Using this function, we can save into the database, not the password itself, but the calculated value of the function from the password. For example, in Java, it could look like this:
String password = "pa$$word";
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(password.getBytes());
String hash = new BigInteger(1, digest).toString(16);
As a result of the conversion we will get the following value, which can be saved in the database:
6a158d9847a80e99511b2a7866233e404b305fdb7c953a30deb65300a57a0655
This variant is already much better, but it still has disadvantages. One of them is that users with the same password will have the same hash. If an intruder gains access to the database, he can use it for his purposes. But the most dangerous thing is the possibility of brute-force passwords. You can make your database (or use an existing one) with the most popular passwords/words and hashes. Thus, you can quickly restore the values of user passwords. That's why this option is also not recommended.
Password Hashing With Unique Salt
To eliminate the disadvantages of the previous solution, it is possible to use a salt that is unique for each user. Salt is a random value concatenated with the password, and a hash function is taken from the result.
String password = "pa$$word";
String salt = "b0f57dccf7133f7ef3acb09641e5f7a3";
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest((password + salt).getBytes());
String hash = new BigInteger(1, digest).toString(16)
The random salt can be generated like this:
Random random = new SecureRandom();
byte[] saltBytes = new byte[16];
random.nextBytes(saltBytes);
String salt = new BigInteger(1, saltBytes).toString(16);
As a result, we solve several problems at once. First, users with the same password will have a different salt value and, consequently, the value of the hash function. It will be much harder for an intruder to pick up passwords because the pre-calculated hash tables will not be able to be applied.
Special Algorithms PBKDF2, BCrypt, SCrypt
The best option would be to use special algorithms developed for hashing passwords. These algorithms are adaptive and allow you to intentionally slow down the computation time to make a brute-force attack more difficult.
Let's take the BCrypt algorithm (the implementation is part of Spring Security) as an example:
String password = "pa$$word";
String salt = BCrypt.gensalt();
String hash = BCrypt.hashpw(password, salt);
The result is the following hash value:
$2a$10$alXdzX7lkEp52xiKS7YfuelpoFqz6AsvyBwIEz/BbWghdkmwGqYoy
$2a$ - the hash algorithm identifier
10 - number of hashing rounds (2^10 = 1024)
alXdzX7lkEp52xiKS7Yfue - salt
lpoFqz6AsvyBwIEz/BbWghdkmwGqYoy - hash
To calculate this function, 1024 hash rounds are used. Over time, as the computational capacity grows, we can increase this value to keep the computation complex.
When authenticating a user, it is enough to call the method for checking the password sent with the value of the hash stored in the database:
//plaintext - sent by the user
//hash - loaded from DB
boolean checkpw = BCrypt.checkpw(plaintext, hash);
Using PAKE
Is it possible to verify that the user knows the password without transmitting it? This can be done with the PAKE (Password Authentication Key Agreement) family of protocols. The algorithm is designed specially so that the password itself is not transmitted, but the values of some calculations that use the password are transmitted. And if an attacker gains access to the database or can even eavesdrop on the channel between the client and the server, he won't be able to restore the original password value. Let's take SRP-6a, which belongs to PAKE, as an example.
The logon process is performed in 2 steps, and after mathematical calculations, it is possible to prove that the client on the server-side knows the password without passing the password. The protocol specification is described in detail in RFC5054. Let's describe the user registration process. To save the authentication data it is necessary to calculate the values of v
and s
, where v
-verifier, s
-salt. We already know how to calculate salt, but the verifier is calculated as follows:
x = H(salt, password)
v = g^x (mod N)
H
- hash function (SHA-1, SHA256, etc.)
g
, N
- constants that can be chosen from RFC5054.Appendix A. It's worth noting that the chosen constants and hash function must be the same on the server and the client.
The salt and verifier values can be computed on the client and the server. If the values are computed on the client, we don't transmit the password at all over the communication channel, but we can't check the password policy (length, number of wildcards, etc.) on the server, so these checks will also need to be transferred to the client-side.
As an example, you can use the Nimbus SRP library:
String password = "pa$$word";
SRP6CryptoParams config = SRP6CryptoParams.getInstance(256, "SHA-1");
SRP6VerifierGenerator verifierGenerator = new SRP6VerifierGenerator(config);
byte[] saltBytes = verifierGenerator.generateRandomSalt(16);
String verifier = verifierGenerator.generateVerifier(saltBytes, password.getBytes()).toString(16);
String salt = new BigInteger(saltBytes).toString(16);
Result:
salt: 6bb9db1c839bdc59ecbcd0ee12488462
verifier: f28aed4372b1312ccdd6e281c7270be503bac99bff845c41da8189eadf9e4497
These values must be saved in the database and used later in the process of client authentication. The great advantage of this protocol is that the password is not transmitted to the server in any way, and you cannot recover the original password from the verifier value. Moreover, the verifier is transmitted only during registration (if computed on the client-side) and used only for computation during authentication. The protocol itself is resistant to MITM attacks, which means that if someone accidentally enables logging of all user requests on the server and those logs are subsequently leaked, it will not compromise passwords at all. This data is calculated within each session and cannot be used for re-entry.
Conclusion
Of course, authentication is not the only aspect that goes into the concept of cybersecurity. But the correct use of modern user authentication methods significantly reduces the chance of compromising authentication data. Particular attention should also be paid to logging requests and storing these logs.
Opinions expressed by DZone contributors are their own.
Comments