Skip to content
Chapter 35 · Spring Boot

Spring Security Part 2: Creating and Storing Users ​

Authentication and authorization can only happen after a user exists. So before the login methods, user creation.

The default user ​

Add only the security starter and start the application:

Using generated security password: dcfcaf08-8149-4e4f-86a6-e1dcb0f2d556

At startup a user is created with the username user and a random password, new on every restart. It comes from SecurityProperties:

java
public static class User {
    private String name = "user";                                // default username
    private String password = UUID.randomUUID().toString();     // random password
    private List<String> roles = new ArrayList<>();
    private boolean passwordGenerated = true;
    // getters and setters
}

and an auto-configuration (UserDetailsServiceAutoConfiguration) puts it into an InMemoryUserDetailsManager, which keeps users in a map in memory:

java
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
        ObjectProvider<PasswordEncoder> passwordEncoder) {
    SecurityProperties.User user = properties.getUser();
    List<String> roles = user.getRoles();
    return new InMemoryUserDetailsManager(
            User.withUsername(user.getName())
                .password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
                .roles(StringUtils.toStringArray(roles))
                .build());
}

Way 1: application.properties (development and testing only) ​

properties
spring.security.user.name=my_username
spring.security.user.password=my_password
spring.security.user.roles=ADMIN

These are bound onto SecurityProperties.User through its setters, replacing the defaults. No random password is generated any more.

Way 2: your own InMemoryUserDetailsManager (development and testing only) ​

For several users, define the UserDetailsService bean yourself:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user1 = User.withUsername("my_username_1")
                .password("{noop}my_password_1")   // {noop}: stored as plain text
                .roles("ADMIN")
                .build();

        UserDetails user2 = User.withUsername("my_username_2")
                .password("{noop}1234")
                .roles("USER")
                .build();

        return new InMemoryUserDetailsManager(user1, user2);
    }
}

Once you define a UserDetailsService, Spring Boot no longer creates the default user.

Why {noop}? ​

Spring stores passwords in the format {id}encodedPassword. The {id} names the algorithm: {noop} (no encoding: plain text), {bcrypt}, {sha256}, and so on.

At login:

  1. The stored password is loaded.
  2. The {id} is read off the front.
  3. {noop}: the rest is compared directly with the password the user typed.
  4. {bcrypt}: the typed password is checked with the BCrypt encoder against the stored hash.

Hashing is one-way: a hash cannot be turned back into the password. So checking a password means hashing what the user typed and comparing hashes.

To store a BCrypt hash:

java
UserDetails user1 = User.withUsername("my_username_1")
        .password("{bcrypt}" + new BCryptPasswordEncoder().encode("my_password_1"))
        .roles("ADMIN")
        .build();

Logging in with my_password_1 works. When no encoder is configured, Spring uses DelegatingPasswordEncoder: it looks at the {id} and delegates to the matching encoder.

Choosing one encoder ​

To avoid the {id} prefix, declare a single PasswordEncoder bean. All passwords then go straight to it, and none needs a prefix:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user1 = User.withUsername("my_username_1")
                .password(new BCryptPasswordEncoder().encode("my_password_1"))
                .roles("ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user1);
    }
}

The flip side: every stored password must now be a BCrypt hash. A {noop}1234 password fails to log in, with "Encoded password does not look like BCrypt" in the log.

Way 3: users in a database (for production) ​

In production, users are created at runtime (a sign-up API) and stored in a database, with hashed passwords.

The entity implements Spring Security's UserDetails. Every authentication method loads the user as a UserDetails; implementing it means no mapping from your entity is needed.

java
@Entity
@Table(name = "user_auth")
public class UserAuthEntity implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    private String role;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority(role));
    }

    @Override
    public String getPassword() { return password; }

    @Override
    public String getUsername() { return username; }

    // isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, isEnabled:
    // true by default in Spring Security 6; override to add such rules

    // setters, getRole()
}

The repository:

java
@Repository
public interface UserAuthEntityRepository extends JpaRepository<UserAuthEntity, Long> {
    Optional<UserAuthEntity> findByUsername(String username);
}

The service implements UserDetailsService. During authentication Spring Security loads the user by name, but it does not know your table, so you implement loadUserByUsername:

java
@Service
public class UserAuthEntityService implements UserDetailsService {

    @Autowired
    private UserAuthEntityRepository userAuthEntityRepository;

    public UserDetails save(UserAuthEntity userAuth) {
        return userAuthEntityRepository.save(userAuth);
    }

    @Override
    public UserAuthEntity loadUserByUsername(String username) throws UsernameNotFoundException {
        return userAuthEntityRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    }
}

The register API hashes the password before saving:

java
@RestController
@RequestMapping("/auth")
public class UserAuthController {

    @Autowired
    private UserAuthEntityService userAuthEntityService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostMapping("/register")
    public ResponseEntity<String> register(@RequestBody UserAuthEntity userAuthDetails) {
        userAuthDetails.setPassword(passwordEncoder.encode(userAuthDetails.getPassword()));
        userAuthEntityService.save(userAuthDetails);
        return ResponseEntity.ok("User registered successfully!");
    }
}

The security configuration. By default every endpoint requires authentication, including /auth/register, which is how a user gets credentials in the first place. So registration must be open to everyone, which is standard practice:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/register").permitAll()   // no authentication
                .anyRequest().authenticated()                     // everything else
            )
            .csrf(csrf -> csrf.disable())                         // explained in the next parts
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }
}
json
POST /auth/register
{"username": "U1", "password": "123", "role": "ROLE_USER"}

U1 / 123 can now log in. The table holds a BCrypt hash such as $2a$10$ZJe1He102I7h12jaPoFLke9Z0rxAkyVNpv2c.qWDwBW/BacxMlKdi: $2a is the BCrypt version, 10 the cost factor. BCrypt adds a random salt to each hash, so two users with the same password get different hashes.

Store roles with the ROLE_ prefix. hasRole("USER") (next part) checks for the authority ROLE_USER. A user registered with "role": "USER" logs in, but gets 403 on endpoints that require the USER role. (User.withUsername(...).roles("USER") adds the prefix for you; a raw SimpleGrantedAuthority does not.)

Next: With users stored, part 3 adds the first login method: form login, Spring Security's default, which keeps the user logged in with a session.

Check yourself ​

  1. With only the security starter, what user exists and what is its password?
  2. How do you set the username, password and roles in application.properties? Is that fine for production?
  3. What happens to the default user when you define your own UserDetailsService?
  4. What does the {id} in {bcrypt}$2a$... mean, and which encoder reads it?
  5. How is a login password checked against a BCrypt hash?
  6. After declaring a BCryptPasswordEncoder bean, a user stored as {noop}1234 cannot log in. Why?
  7. Why does the entity implement UserDetails, and why does the service implement UserDetailsService?
  8. Why must /auth/register be permitAll()?
  9. Why do two users with the same password have different hashes?
  10. A user is stored with role USER and gets 403 on an endpoint requiring hasRole("USER"). Why?
Answers
  1. user, with a random password printed at startup (new each restart).
  2. spring.security.user.name, .password, .roles. No, only for development and testing.
  3. It is no longer created.
  4. The algorithm the password was encoded with; DelegatingPasswordEncoder (the default) delegates to that encoder.
  5. The typed password is hashed with BCrypt (using the salt from the stored hash) and compared; hashing is one-way.
  6. That encoder is now used for every password and expects a BCrypt hash ("Encoded password does not look like BCrypt").
  7. Spring Security works with UserDetails objects, so no mapping is needed; and it needs loadUserByUsername to fetch users from your table.
  8. Every endpoint needs authentication by default, and a new user has no credentials yet.
  9. BCrypt adds a random salt to each hash.
  10. hasRole("USER") checks for the authority ROLE_USER; store the role as ROLE_USER.