Password Encoding with Spring Security

Introduction

Password Encoding is the process in which a password is converted from a literal text format into a humanly unreadable sequence of characters. If done correctly, it is very difficult to revert back to the original password and so it helps secure user credentials and prevent unauthorized access to a website.

There are many ways to encode a password - encryption, hashing, salting, slow hashing...

That being said, password encoding is a very important aspect of every application and should be taken seriously as one of the basic steps we take to secure an application's user's personal information and data.

PasswordEncoder is a Spring Security interface which contains a very flexible mechanism when it comes to password storage.

Outdated Security Mechanisms

Literal Values

In the not so distant past, passwords were stored in literal text format in databases without any encoding or hashing. As databases need authentication, which nobody except the admins and the application had, this was considered safe.

Quickly, SQL Injections and SQL Obfuscations, as well as other attacks, surfaced. These kinds of attacks relied on outside users getting view-privileges for certain database tables.

Since the passwords were stored bluntly in text format, this was quite enough for them to get a hold of all the passwords and use them right away.

Encryption

Encryption is a safer alternative and the first step taken towards password security. Encrypting a password relies on two things:

  • Source - The password input during registration
  • Key - A random key generated by the password

Using the key, we can perform a two-way transformation on the password - both encrypt and decrypt it.

That fact alone is the liability of this approach. As the keys were often stored on the same server, it was common for these keys to fall into the wrong hands, that now had the ability to decrypt passwords.

Hashing

To combat these attacks, developers had to come up with a way to protect passwords in a database in such a way that they can't be decrypted.

The concept of one-way hashing was developed and some of the most popular hashing functions at the time were MD5, SHA-1, SHA-256.

However, these strategies did not remain effective, since attackers started storing the known hashes with well-known passwords and passwords obtained from major leaks of social media.

The stored passwords were saved in lookup tables called rainbow tables and some popular ones contained millions upon millions of passwords.

The most popular one – RockYou.txt contains over 14 million passwords for over 30 million accounts. Funny enough, almost 300,000 of those used the password "123456".

This is still a popular approach and many applications still simply hash the passwords using well-known hashing functions.

Salting

To combat the appearance of rainbow tables, developers started adding a random sequence of characters to the beginnings of the hashed passwords.

While it wasn't a complete game changer, it at least slowed down attackers since they couldn't find hashed versions of passwords in public rainbow tables. So if you had a common password like "123456", the salt would prevent your password from being identified immediately since it was changed before hashing.

Slow Hashing

Attackers can exploit pretty much any feature you can think of. In the previous cases, they exploited the speed of hashing, which even led to brute-force hashing and comparison of passwords.

A very easy and simple fix for this problem is implementing slow hashing - Algorithms like BCrypt, PBKDF2, Scrypt, etc. salt their hashed passwords and slow down after a certain iteration count, making brute-force attacks extremely difficult due to the amount of time it takes to compute a single hash. The time it takes to compute a hash can take anywhere from a few milliseconds to a few hundred milliseconds, depending on the number of iterations used.

Password Encoders

Spring Security provides multiple password encoding implementations to choose from. Each have their advantages and disadvantages, and a developer can choose which one to use depending on the authentication requirement of their application.

BCryptPasswordEncoder

BCryptPasswordEncoder relies on the BCrypt algorithm to hash passwords, which was described earlier.

A constructor parameter to keep an eye out for here is the strength. By default, it's set to 10, though it can go up to 32 - The larger the strength is, the more work it takes to compute the hash. This "strength" is actually the number of iterations (210) used.

Another optional argument is SecureRandom. SecureRandom is an object containing a random number that's used to randomize the generated hashes:

// constructors
BCryptPasswordEncoder()
BCryptPasswordEncoder(int strength)
BCryptPasswordEncoder(int strength, java.security.SecureRandom random)

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12); // Strength set as 12
String encodedPassword = encoder.encode("UserPassword");

This is how a hashed password looks like:

$2a$12$DlfnjD4YgCNbDEtgd/ITeOj.jmUZpuz1i4gt51YzetW/iKY2O3bqa

A couple of things to keep in mind regarding the hashed password is:

Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder relies on the PBKDF2 algorithm to hash passwords.

It has three optional arguments:

  • Secret - Key used during the encoding process. As the name implies, it should be secret.
  • Iteration - The number of iterations used to encode the password, the documentation advises as many iterations for your system to take 0.5 seconds to hash.
  • Hash Width - The size of the hash itself.

A secret is object type of java.lang.CharSequence and when a developer supplies it to the constructor, the encoded password will contain the secret.

// constructors
Pbkdf2PasswordEncoder()
Pbkdf2PasswordEncoder(java.lang.CharSequence secret)
Pbkdf2PasswordEncoder(java.lang.CharSequence secret, int iterations, int hashWidth)

Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder("secret", 10000, 128);
String encodedPassword = encoder.encode("UserPassword");

This is how a hashed password looks like:

zFRsnmzHKgWKWwgCBM2bfe0n8E9EZRsCtngcSBewray7VfaWjeYizhCxCkwBfjBMCGpY1aN0YdY7iBNmyiT+7bdfGfCeyUdGnTUVxV5doJ5UC6m6mj2n+60Bj8jGBMs2KIMB8c/zOZGLnlyvlCH39KB5xewQ22enLYXS5S8TlwQ=

An important thing to note here is the hash length that we can directly influence.

We can define a short hash (5):

zFRsnmw=

Or a really long one (256):

zFRsnmzHKgWKWwgCBM2bfe0n8E9EZRsCtngcSBewray7VfaWjeYizhCxCkwBfjBMCGpY1aN0YdY7iBNmyiT+7bdfGfCeyUdGnTUVxV5doJ5UC6m6mj2n+60Bj8jGBMs2KIMB8c/zOZGLnlyvlCH39KB5xewQ22enLYXS5S8TlwQMmBkAFQwZtEdYpWySRTmUFJRkScXGev8TFkRAMNHoceRIf8eF/C9VFH0imkGuxA7r2tJlyo/n0vLNan6ZBngt76MzgF+S6SCNqGwUn5IWtfvkeL+Jyz761LI39sykhVGp4yTxLLRVmvKqqMLVOrOsbo9xAveUOkIzpivqBn1nQg==

The longer the output, the safer the password, right?

Yes, but please keep in mind - It's safer up to a certain point, after which, it simply becomes an overkill. There's usually no need to hash beyond 2128 as it's already a hash that's practically unbreakable with modern technology and computing power.

SCryptPasswordEncoder

SCryptPasswordEncoder relies on the SCrypt algorithm to hash passwords.

The output of its constructor is a derived key which is actually a password-based key used to store in the database. The constructor call has optional arguments:

  • CPU cost - CPU Cost of the algorithm, the default is 214 - 16348. This int must be a power of 2.
  • Memory cost - By default is 8
  • Parallelization - Although formally present, SCrypt doesn't take advantage of parallelization.
  • Key Length - Defines the length of the output hash, by default, it's set to 32.
  • Salt Length - Defines the length of the salt, the default value is 64.

Please keep in mind that SCryptPasswordEncoder is rarely used in production. This is partly due to the fact that it originally wasn't designed for password storage.

Although controversial, giving "Why I Don't Recommend Scrypt" a read might help you choose.

// constructors
SCryptPasswordEncoder()
SCryptPasswordEncoder(int cpuCost, int memoryCost, int parallelization, int keyLength, int saltLength)

SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String encodedPassword = encoder.encode("UserPassword");

This is how a hashed password looks like:

e8eeb74d78f59068a3f4671bbc601e50249aef05aae1254a2823a7979ba9fac0

DelegatingPasswordEncoder

The DelegatingPasswordEncoder provided by Spring delegates to another PasswordEncoder using a prefixed identifier.

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

In the software industry, many applications still use old password encoders. Some of these cannot be easily migrated to newer encoders and technologies although the passage of time warrants new technologies and approaches.

The DelegatingPasswordEncoder implementation solves many problems including the one we discussed above:

  • Ensuring that passwords are encoded using the current password storage recommendations
  • Allowing for upgrading the encoders in the future
  • Easy construction of an instance of DelegatingPasswordEncoder using PasswordEncoderFactories
  • Allowing for validating passwords in modern and legacy formats
Map encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());

PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder("bcrypt", encoders);
passwordEncoder.encode("UserPassword");

In the constructor call, we pass two arguments:

  • (String) "bcrypt" - Password encoder ID as a String
  • (HashMap) encoders - A map which contains a list of encoders

Each row of the list contains an encoder type prefix in String format and its respective encoder.

This is how a hashed password looks like:

$2a$10$DJVGD80OGqjeE9VTDBm9T.hQ/wmH5k3LXezAt07EHLIW7H.VeiOny

During authentication, the user-provided password is matched with the hash, as usual.

Demo Application

Now with all of that out of the way, let's go ahead and build a simple demo application that uses BCryptPasswordEncoder to hash a password upon registration. The same process would go for all other encoders, as seen above.

Dependencies

Like with all Spring and Spring Boot projects, let's start off with the needed dependencies:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-core</artifactId>
        <version>{version}</version>
    </dependency>

    <!--OPTIONAL DEPENDENCY NEEDED FOR SCRYPTPASSWORDENCODER-->
    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcprov-jdk15on</artifactId>
        <version>{version}</version>
        <optional>true</optional>
    </dependency>
</dependencies>

With our dependencies taken care of, let's go ahead and test out our encoder of choice:

@SpringBootApplication
public class DemoApplication {

   public static void main(String[] args) {
      SpringApplication.run(DemoApplication.class, args);
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16); // Strength set as 16
        String encodedPassword = encoder.encode("UserPassword");
        System.out.println("BCryptPasswordEncoder");
        System.out.println(encodedPassword);
        System.out.println("\n");
   }
}

Running this piece of code would yield:

$2a$16$1QJLYU20KANp1Vzp665Oo.JYrz10oA0D69BOuckubMyPaUO3iZaZO

It's running correctly using BCrypt, please keep in mind that you can use any other password encoder implementation here, they're all imported within spring-security-core.

XML-Based Configuration

One of the ways you can configure your Spring Boot application to use a password encoder upon login is relying on the XML-based configuration.

In the .xml file you've already defined your Spring Security configuration, withing your <authentication-manager> tag, we'll have to define another property:

 <authentication-manager>
        <authentication-provider user-service-ref="userDetailsManager">
            <password-encoder ref="passwordEncoder"/>
        </authentication-provider>
    </authentication-manager>

    <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
        <!--Optional tag, setting the strength to 12 -->
        <constructor-arg name="strength" value="12"/>
    </bean>

    <bean id="userDetailsManager" class="org.springframework.security.provisioning.JdbcUserDetailsManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

Java-Based Configuration

We can also configure the password encoder in Java-based configuration file:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    DataSource dataSource;

    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder auth)
        throws Exception {

        auth.jdbcAuthentication().dataSource(dataSource)
            .passwordEncoder(passwordEncoder())
            .usersByUsernameQuery("{SQL}") //SQL query
            .authoritiesByUsernameQuery("{SQL}"); //SQL query
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        return encoder;
    }

User Model

With all of the configuration of the application done, we can go ahead and define a User model:

@Entity
public class User {

    @Id
    @GeneratedValue
    private int userId;
    private String username;
    private String password;
    private boolean enabled;

    // default constructor, getters and setters
}

The model itself is quite simple, containing some of the basic information we'd need to save it into the database.

Service Layer

The whole service layer is taken care of by UserDetailsManager for brevity and clarity. For this demo, there's no need to define a custom service layer.

This makes it very easy to save, update and delete users for the purpose of this demo, though I personally recommend to define your custom service layer in your applications.

Controller

The controller has two jobs - allowing users to register, and allowing them to log in afterwards:

@Controller
public class MainController {

    @Autowired
    private UserDetailsManager userDetailsManager;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @RequestMapping("/")
    public String index() {
        return "index";
    }

    @RequestMapping("/register")
    public String test(Model model) {
        User user = new User();
        model.addAttribute("user", user);
        return "register";
    }

    @RequestMapping(value = "register", method = RequestMethod.POST)
    public String testPost(@Valid @ModelAttribute("user") User user, BindingResult result, Model model) {
        if (result.hasErrors()) {
            return "register";
        }
        String hashedPassword = passwordEncoder.encode(user.getPassword());

        Collection<? extends GrantedAuthority> roles = Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));

        UserDetails userDetails = new User(user.getUsername(), hashedPassword, roles);
        userDetailsManager.createUser(userDetails);
        return "registerSuccess";

 @RequestMapping("/login")
    public String login(
            @RequestParam(value = "error", required = false) String error,
            @RequestParam(value = "logout", required = false) String logout, Model model) {
        if (error != null) {
            model.addAttribute("error", "Wrong username or password!");
        }

        if (logout != null) {
            model.addAttribute("msg", "You have successfully logged out.");
        }
        return "login";
    }
  }
}

Upon receiving a POST request, we fetch the User info and hash the password using our encoder.

After this, we simply grant an authority to our registered user and pack the username, hashed password and authority together into a single object using UserDetails - Again, for brevity and simplicity of the demo application.

View

Now, to round everything up, we need a few simple views to make our application functional:

  • index - The main/index page of the application
  • register - A page with a registration form that accepts a username and password
  • registerSuccess - An optional page that displays a success message if the registration is complete
  • login - A page that allows the registered users to log in

Index

<html>
    <head>
        <title>Home</title>
    </head>
    <body>
        <c:if test="${pageContext.request.userPrincipal.name == null}">
            <h1>Please <a href="/login">login</a> or <a href="/register">register</a>.</h1>
        </c:if>

        <c:if test="${pageContext.request.userPrincipal.name != null}">
            <h1>Welcome ${pageContext.request.userPrincipal.name}! | <a href="<c:url value="/j_spring_security_logout"/>">Logout</a></h1>
        </c:if>
    </body>
</html>

Register

<html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <h2>Please fill in your credentials to register:</h2>

        <form:form action="${pageContext.request.contextPath}/register" method="post" modelAttribute="user">
            <h4>Username</h4>
            <label for="username">Username: </label>
            <form:input path="username" id="username"/>

            <h4>Password</h4>
            <label for="password">Password: </label>
            <form:password path="password" id="password"/>

            <input type="submit" value="Register">
        </form:form>
    </body>
</html>

Note: In previous versions of Spring, it was common practice to use commandName rather than modelAttribute, though in the newer versions, it's encouraged to use the new approach.

Registration Successful

<html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <h1>You have registered successfully!</h1>
    </body>
</html>

Login

<html>
<head>
    <title>Login</title>
</head>
    <body>
        <div id="login-box">
            <h2>Log in using your credentials!</h2>

            <c:if test="${not empty msg}">
                <div class="msg">
                        ${msg}
                </div>
            </c:if>

            <form name="loginForm" action="<c:url value="/j_spring_security_check"/>" method="post"">

                <c:if test="${not empty error}">
                    <div class="error" style="color:red">${error}</div>
                </c:if>

                <div class="form-group">
                    <label for="username">Username: </label>
                    <input type="text" id="username" name="username" class="form-control"/>

                </div>

                <div class="form-group">
                    <label for="password">Password: </label>
                    <input type="password" id="password" name="password" class="form-control"/>
                </div>

                <input type="submit" value="Login" class="btn btn-default"/>
                <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

            </form>
        </div>
    </body>
</html>

Note: j_spring_security_check was replaced with login, though most people still haven't migrated to Spring Security 4, where it was introduced. To avoid confusion, I've included the old keyword, though it won't work if you're using the new version of Spring Security.

Testing the Application

Let's go ahead and start up our application to test whether it's working fine.

Since we're not logged in, the index page is asking us to either register or log in:

Upon redirecting to the register page, we can input our information:

Everything proceeded smoothly, and we're prompted with a successful registration page:

Taking a step back, at the database, we can notice a new user, with a hashed password:

The added user also has a ROLE_USER, as defined in the controller:

We can now jump back into the application and try to log in:

Upon entering the correct credentials, we're greeted with our index page once again, but this time with a different message:

Conclusion

Spring Security's implementations of the popular hashing algorithms work like a charm, provided that the user doesn't pick a really bad password. We've discussed the need for password encoding, some outdated approaches to protecting passwords from potential attackers and the implementations we can use to do so in a more safe and modern approach.

In the end, we've made a demo application to show BCryptPasswordEncoder in use.

Last Updated: October 24th, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

David LandupAuthor

Entrepreneur, Software and Machine Learning Engineer, with a deep fascination towards the application of Computation and Deep Learning in Life Sciences (Bioinformatics, Drug Discovery, Genomics), Neuroscience (Computational Neuroscience), robotics and BCIs.

Great passion for accessible education and promotion of reason, science, humanism, and progress.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms