There is no PasswordEncoder mapped for the id

Upasana | July 25, 2020 | 5 min read | 4,247 views | Spring Boot 2


After migration to Spring Boot 2, you may encounter the below error when you try to hit a protected endpoint with a username and password.

Environment
  1. Spring Boot 2.3.2.RELEASE

  2. Spring Security 5.3.3.RELEASE

Curl Request
curl -X GET http://localhost:8080/api/client -H 'Authorization: Basic Y29hY2hpbmdfd2JjppVU5EeTlVdFg4eTQzTkIxMmg=' -H 'Content-Type: application/json'
Server Response
{
    "timestamp": "2019-07-09T09:02:51.637+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "There is no PasswordEncoder mapped for the id \"null\"",
    "path": "/api/wbc/client"
}
Server Error Logs
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)

Here is the Spring Security Java Configuration:

WebSecurityConfig
@Configuration
@Order(1)
public class WbcSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                    .withUser("user").password("password1").roles("USER")
                .and()
                    .withUser("admin").password("password2").roles("ADMIN");
    }

    protected void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/api/**")
                .authorizeRequests()
                .anyRequest().hasRole("USER")
                .and()
                .httpBasic()
                .and()
                .csrf()
                .disable()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

Solutions

Prior to Spring Security 5.0, the default PasswordEncoder was NoOpPasswordEncoder which required plain text passwords but is insecure. Spring Security 5.x onwards, the default PasswordEncoder is DelegatingPasswordEncoder, which requires a Password Storage Format. You can find more details on migrating to Spring Security 5 this blog Migrating to Spring Security 5

Password Storage Format

The general format for a password is:

{id}encodedPassword

where:

"id"

is an identifier used to look up which PasswordEncoder should be used.

"encodedPassword"

is the original encoded password for the selected PasswordEncoder.

Most commonly used PasswordEncoders with their id’s are:

  1. "noop" which uses plain text NoOpPasswordEncoder

  2. "bcrypt" which uses `BCryptPasswordEncoder'

  3. "scrypt" which uses SCryptPasswordEncoder

  4. "pbkdf2" which uses 'Pbkdf2PasswordEncoder'

  5. "sha256" which uses StandardPasswordEncoder

Example of a Password that is encoded using bcrypt is:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

DelegatingPasswordEncoder solves many of the challenges:

  1. Allows validating passwords in modern and legacy formats.

  2. Allows for upgrading to a newer encoding in the future.

  3. Ensures that passwords are encoded using the current password storage recommendations.

We can easily create an instance of DelegatingPasswordEncoder using the below code:

PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

or infact, you can create a bean for the same:

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

Solution 1. Add password storage format

We can add password storage format, which is {noop} for plain text passwords.

The new WebSecurityConfig will look like this:

import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;

@Configuration
@Order(1)
public class WbcSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().
                    withUser("user").password("{noop}password1").roles("USER")
                .and()
                    .withUser("admin").password("{noop}password2").roles("ADMIN");   (1)
    }

    protected void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/api/**")
                .authorizeRequests()
                .anyRequest().hasRole("USER_ROLE")
                .and()
                .httpBasic()
                .and()
                .csrf()
                .disable()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
1 Here we are telling Spring Security that NoOpPasswordEncoder shall be used to decode the password. We shall use {bcrypt} instead which is more secure way of storing password in source code files.

Now everything shall work fine.

If you want to programmatically generate passwords in storage format, you can use the below code snippet:

Generate password using default password encoder (Bcrypt for now)
User user = User.withDefaultPasswordEncoder()
  .username("user")
  .password("password")
  .roles("user")
  .build();
System.out.println(user.getPassword());
Sample output
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

Using Spring Boot CLI

Optionally, you can use Spring Boot CLI to quickly encode your password into storage format. Follow the instructions below to use Spring Boot CLI.

Install SDKMAN with one command
curl -s "https://get.sdkman.io" | bash

If everything goes good, check if SDKMAN is correctly installed:

sdk --version

Now install spring boot cli

sdk install springboot

Check the version

spring --version

Encrypt a given plain password:

spring encodepassword foo
Output
{bcrypt}$2a$10$g08xa7qe52RUj8PgS3s4U.2SPFuKyuxKo8d6eEaQB4s04TKbkhe.C

Solution 2. Create a UserDetailsService Bean (not preferred)

We can use User.withDefaultPasswordEncoder() for UserDetailsService that will automatically take care of hashing the password in required format.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class WbcSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public UserDetailsService userDetailsService() {
        User.UserBuilder users = User.withDefaultPasswordEncoder();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(users.username("user").password("password1").roles("USER").build());
        manager.createUser(users.username("admin").password("password2").roles("ADMIN").build());
        return manager;

    }

}
This does hash the password that is stored, but the passwords are still exposed in memory and in the compiled source code. Therefore, it is still not considered secure for a production environment. For production, you should always hash your passwords externally.

Solution 2. Switching to older behavior (insecure)

Optionally, we can switch back to NoOpPasswordEncoder by declaring a bean using below code:

@Config
public class SecurityConfig {

    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

or, even you can do the same using below code:

@Configuration
@Order(1)
public class WbcSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(NoOpPasswordEncoder.getInstance())     (1)
                    .withUser("user").password("{noop}password1").roles("USER")
                .and()
                    .withUser("admin").password("{noop}password2").roles("ADMIN");
    }

    // rest of the code
}
1 We are intentionally reverting to the previous behavior of Spring Security

But both of these are insecure solutions and should not be preferred, as suggested by Spring Docs.

Reverting to NoOpPasswordEncoder is not considered to be secure. You should instead migrate to using DelegatingPasswordEncoder to support secure password encoding.

What is NoOpPasswordEncoder

NoOpPasswordEncoder is a password encoder that does nothing. It has been deprecated now. It is useful for testing where working with plain text passwords is preferred.

As per spring docs

This NoOpPasswordEncoder is provided for legacy and testing purposes only and is not considered secure.


Top articles in this category:
  1. Multi-threading Java Interview Questions for Investment Bank
  2. What is difference between Primary key and Unique Key
  3. SQL - Write a query to find customers who have no orders yet
  4. Difference between getOne and findById in Spring Data JPA?
  5. Reverse the bits of a number and check if the number is palindrome or not
  6. Sapient Global Market Java Interview Questions and Coding Exercise
  7. What is difference between HTTP Redirect and Forward

Recommended books for interview preparation:

Find more on this topic: