1. Introduction
Spring Boot has inbuilt support for authentication. The most common way of authentication is using username and password. In this tutorial, we’ll discuss about authentication using username and password and encoding passwords. We’ll discuss in brief about the password encoding. Understanding the background of password storage is very important to secure an application.
2. Password storage evolution
Initially the passwords were saved as plain text. Anyone who had the access to the resource used to store the passwords could see all the passwords. There are so many incidents in history where malicious users were able to get access to all the saved passwords through various attacks like SQL Injection.
Since passwords in plain text were easy to read by everyone, developers started to save passwords as using hash algorithms as SHA-256. During authentication, the user provided password was passed through hash algorithm and the hash value was matched with the saved hash value. When developers started using one-way hash like SHA-256, malicious users found ways to crack the passwords. From the hashes, it was very difficult to compute the original passwords. So hackers created rainbow tables, which stored hash value of passwords. Instead of computing password from hashes, hackers used rainbow tables to guess the passwords with less computation.
Developers then started to use salted passwords. Salted password mechanism can be explained with the help of following points:
- Salt is random bytes generated for each password.
- Salt is saved alongside every hashed password in the database.
- During encryption, first a salt is generated for the password. This salt and password are then passed through a hash function to generate another hash value. This generated hash value is saved in database along side the salt.
- When a user provides a password for authentication, password and hash are passed through hash function. The generated hash value is compared with the hash value saved in database. If values match, user is authenticated else not.
3. Password storage in present time
With the technology and hardware we have now a days, SHA-256 passwords can be cracked. Developers are using adaptive one-ways functions to make their applications more secure. Example of adaptive one-way functions are bcrypt
, PBKDF2
, scrypt
, and argon2
. Adaptive one-way functions are compute intensive and require resources. These functions are made resource intensive intentionally to make them costly to crack passwords.
Adaptive one-way functions have a term work factor. Work factor can be changed according to the hardware. The recommended value to set the work factor is such that the system takes 1 second to verify a password.
Most password encoding and matching mechanism is based on one way transformation. It means passwords are transformed into a value but you don’t get the password again from the value. When user provides password for authentication, provided password is transformed and matched with the saved value. There is no step in authentication where password is retrieved from transformed value.
4. PasswordEncoder interface
This is service interface for encoding passwords. All commonly used password encoding classes implement this interface. Following class implement PasswordEncoder
interface:
- AbstractPasswordEncoder
- Argon2PasswordEncoder
- BCryptPasswordEncoder
- DelegatingPasswordEncoder
- LdapShaPasswordEncoder
- Md4PasswordEncoder
- MessageDigestPasswordEncoder
- NoOpPasswordEncoder
- Pbkdf2PasswordEncoder
- SCryptPasswordEncoder
- StandardPasswordEncoder
5. DelegatingPasswordEncoder
Following are two major problems related to password storage:
- Password storage best practices change with time. When malicious users are able to compromise the prevalent ways, new practices of password storage are introduced.
- Migrating to new password storage practices may break the application.
So when you think about securing your application, your application should be able to upgrade to new encoding in future.
To fix these problems, Spring Security introduced DelegatingPasswordEncoder
. This class delegates password encoding responsibility to another PasswordEncoder
. This delegation is done using a prefixed identifier.
You can create an instance of DelegatingPasswordEncoder
using PasswordEncoderFactories
class or using constructor.
5.1 DelegatingPasswordEncoder instance using PasswordEncoderFactories
PasswordEncoderFactories
class provides a method createDelegatingPasswordEncoder()
which creates a DelegatingPasswordEncoder
with default mappings. The default mappings are:
- bcrypt – BCryptPasswordEncoder
- ldap – LdapShaPasswordEncoder
- MD4 – Md4PasswordEncoder
- MD5 – new MessageDigestPasswordEncoder(“MD5”)
- noop – NoOpPasswordEncoder
- pbkdf2 – Pbkdf2PasswordEncoder
- scrypt – SCryptPasswordEncoder
- SHA-1 – new MessageDigestPasswordEncoder(“SHA-1”)
- SHA-256 – new MessageDigestPasswordEncoder(“SHA-256”)
- sha256 – StandardPasswordEncoder
- argon2 – Argon2PasswordEncoder
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
5.2 DelegatingPasswordEncoder instance using constructor
DelegatingPasswordEncoder
class provides a constructor as well. The first parameter to the constructor is the “id” for encoding. The second parameter to the constructor is a Map
which contains mapping of “id” with PasswordEncoder
. The default mappings are mentioned above.
String idForEncode = "bcrypt"; Map<String,PasswordEncoder> encoderMappings = new HashMap<>(); encoderMappings.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoderMappings.put(idForEncode, new BCryptPasswordEncoder()); encoderMappings.put("noop", NoOpPasswordEncoder.getInstance()); encoderMappings.put("sha256", new StandardPasswordEncoder()); PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoderMappings);
6. Password Storage Format
The general format for a encoded password is:
{id}encodedPassword
For example,
{bcrypt}$2a$10$8bWJaSu2IKSn9Ztxy.50cPPbbu9.FGHiiuttlLy.gjtn/CE
{noop}password
{pbkdf2}5d923baSu2IKSn9ZtaSu2IKSn9ZtaSu2IKSn9ZtaSu2IKSn9ZtaSu2IKSn9ZtaSu2IKSn9ZtaSu2IKSn9Ztc
{sha256}97cde38028adPPbbPjtn4jnPbbjdufkdh5if7yfb9x0kPPjtnPjtnjtn9ZIKSn9jtnZIjtn9ZIKSn9Z0
Here, values between ‘{‘ and ‘}’ are “id” used to identify which encoder to be used. After the “id” is the encoded password. “id” should always be at the beginning of the password. If the “id” cannot be found, the “id” will be null.
In case of this example while matching, the first one will be delegated to BCryptPasswordEncoder
, second to NoOpPasswordEncoder
, third to Pbkdf2PasswordEncoder
and the last one to StandardPasswordEncoder
.
Password matching is done based upon the “id” and the mapping of the “id” to the PasswordEncoder
provided in the constructor.
SCryptPasswordEncoder scryptEncoder = new SCryptPasswordEncoder(); String result = encoder.encode("testPassword"); assertTrue(encoder.matches("testPassword", result));
Spring Security uses
DelegatingPasswordEncoder
by default.
7. Conclusion
In this quick tutorial, we discussed password encoding. We discussed password encoders in brief. In the upcoming tutorials, we’ll see how these encoders are used.