Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Users and passwords

The user account with a random password does not suit us: Anna, Oleg and everyone who registered should log in. Their data already lives in the users table. We need to add a password and a role, store the password safely, and tell Spring Security where to look for a user at login.

A password is never stored as is

The database gets not the password itself but its hash — the result of a one-way transformation. At login the entered password is hashed again and compared. If the database is stolen, there are no passwords in it. Hashing is the job of a PasswordEncoder:

// src/main/java/com/example/blog/SecurityConfig.java
@Configuration
public class SecurityConfig {
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

This encoder uses the bcrypt algorithm and writes its name at the start of the hash: {bcrypt}$2a$10$.... If the algorithm ever changes, old passwords can still be checked.

Password and role in the entity

The columns come from the V2__add_password_and_role.sql migration in the Flyway lesson. User gets these fields:

// src/main/java/com/example/blog/User.java
@Column(nullable = false, length = 100)
private String password;
 
@Column(nullable = false, length = 20)
private String role = "USER";
 
public User(String username, String password) {
    this.username = username;
    this.password = password;
}
 
// plus getters getPassword(), getRole()

Registration

// src/main/java/com/example/blog/NewUserRequest.java
public record NewUserRequest(
        @NotBlank @Pattern(regexp = "[a-z0-9_]{3,20}") String username,
        @NotBlank @Size(min = 8, max = 72) String password) {
}
// src/main/java/com/example/blog/UserController.java
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse register(@Valid @RequestBody NewUserRequest request) {
    if (users.existsByUsername(request.username())) {
        throw new IllegalArgumentException("Username is taken: " + request.username());
    }
    String hash = passwordEncoder.encode(request.password());
    return UserResponse.from(users.save(new User(request.username(), hash)));
}

The controller receives the PasswordEncoder in its constructor, like any bean. The 72-character limit is a bcrypt quirk.

Where Spring Security gets users

// src/main/java/com/example/blog/BlogUserDetailsService.java
@Service
public class BlogUserDetailsService implements UserDetailsService {
 
    private final UserRepository users;
 
    public BlogUserDetailsService(UserRepository users) {
        this.users = users;
    }
 
    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = users.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException(username));
 
        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .roles(user.getRole())
                .build();
    }
}

At login Spring Security calls loadUserByUsername and compares the passwords through the PasswordEncoder. Spring Security's User class is written with its full package name so it is not confused with our entity. As soon as such a bean exists, the temporary password disappears from the log.

Registration is open to everyone

So that people can register without logging in, let's add the first API rules to SecurityConfig:

// src/main/java/com/example/blog/SecurityConfig.java
@Bean
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
    http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers(HttpMethod.POST, "/api/users").permitAll()
                    .anyRequest().authenticated())
            .httpBasic(Customizer.withDefaults())
            .csrf(csrf -> csrf.disable());
    return http.build();
}

The next lesson explains these lines in detail. Let's check:

curl -X POST http://localhost:8080/api/users -H "Content-Type: application/json" \
  -d '{"username":"anna","password":"changeme"}'
curl -X POST http://localhost:8080/api/users -H "Content-Type: application/json" \
  -d '{"username":"ivan","password":"123"}'
{"id":1,"username":"anna"}
{"detail":"Validation failed","instance":"/api/users","status":400,"title":"Bad Request","errors":{"password":"size must be between 8 and 72"}}

In the users table Anna's password looks like {bcrypt}$2a$10$... — changeme itself is stored nowhere. Now curl -u anna:changeme http://localhost:8080/api/posts lets Anna in.

Common mistake

If you write a password into the database without the encoder, for example by hand with SQL, login fails with a 500 error, and the log says:

java.lang.IllegalArgumentException: Given that there is no default password encoder configured, each password must have a password encoding prefix. Please either prefix this password with '{noop}' or set a default password encoder in `DelegatingPasswordEncoder`.

The encoder does not know how a password without a prefix in curly braces was hashed. Create users only through passwordEncoder.encode(...).

Thanks for reading!

Donate to the site — we work hard for you.