DEV Community

Vincent Caunegre
Vincent Caunegre

Posted on • Edited on

Basic authentication in Spring Boot

Spring Security is responsible for authenticate and authorize Spring applications, it provide multiple way of doing authentication but Basic is probably the simplest.

With basic authentication, for every request made to the api, we send the user credentials in the headers, generally encoded with Base64 format.

Creating the Spring Boot application

We keep it simple with only spring web and spring security.

Image description

We will add a simple GET request:

package com.example.spring_basic;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @GetMapping("/hello")
    public String hello(){
       return "hello world !";
    }
}
Enter fullscreen mode Exit fullscreen mode

If we run the application and we inspect the logs, we can see that Spring is already handling security with a generated password

Image description

We can already test the request, for instance with IntelliJ Http request, and see that it works:

### GET request to example server
GET http://localhost:8080/hello
Authorization: Basic user <Generated password>
Enter fullscreen mode Exit fullscreen mode

However this is not what we want, so let's add a new Spring Security Configuration:

package com.example.spring_basic;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.httpBasic(withDefaults());
        httpSecurity.authorizeHttpRequests(http ->{
            http.requestMatchers("/users").permitAll();
            http.anyRequest().authenticated();
        });
        return httpSecurity.build();
    }

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

    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
        UserDetails user= User.builder().username("user")
                .password(passwordEncoder().encode("password"))
                .roles("USER").build();

        UserDetails admin=User.builder().username("admin")
                .password(passwordEncoder().encode("password"))
                .roles("USER","ADMIN").build();
        return new InMemoryUserDetailsManager(user, admin);
    }
}

Enter fullscreen mode Exit fullscreen mode

First we recreate a SecurityFilterChain bean, for now we only add basic auth and require all requests to be authenticated.

Then we declare a PasswordEncoder bean that will be used to encode the password during the security process.

Then we declare a UserDetails implementation. For the moment we will not create custom UserDetails and User. You can see that we use the passwordEncoder to encode the "password" value. So when we will compare the provided password with the user password we will compare the encoded version of the password.

We can test the request by providing credentials:

### GET request to example server
GET http://localhost:8080/hello
Authorization: Basic user password
Enter fullscreen mode Exit fullscreen mode

Use a database to store user

If we want to use a real database we must first add dependencies, in this case we will use spring data jpa and h2 file database

in pom.xml:

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

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>2.3.232</version>
        </dependency>
Enter fullscreen mode Exit fullscreen mode

With this in application.properties

spring.datasource.url=jdbc:h2:file:./data/demo
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
Enter fullscreen mode Exit fullscreen mode

We will add a new User. We will implement UserDetails interface and to keep it simple, we will not add roles in our user case.


@Entity
@Table(name = "users")
public class User implements UserDetails {

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

    public Long getId() {
        return id;
    }

    public User setId(Long id) {
        this.id = id;
        return this;
    }

    public String getUsername() {
        return username;
    }

    public User setUsername(String username) {
        this.username = username;
        return this;
    }

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

    public String getPassword() {
        return password;
    }

    public User setPassword(String password) {
        this.password = password;
        return this;
    }
}
Enter fullscreen mode Exit fullscreen mode

We will create a repository for this user:

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

Enter fullscreen mode Exit fullscreen mode

And a Controller to create new users. Pay attention to inject a passwordEncoder to encrypt the password before saving it to database:

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserController(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @PostMapping
    public User createUser(@RequestBody User user) {
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        return userRepository.save(user);
    }
}
Enter fullscreen mode Exit fullscreen mode

We will remove the InMemoryUserDetailsManager bean inside the SecurityConfig file, and create a new UserDetailService class

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username).orElse(null);
    }
}
Enter fullscreen mode Exit fullscreen mode

With this http request

POST http://localhost:8080/users
Content-Type: application/json

{
  "username": "user",
  "password": "password"
}
Enter fullscreen mode Exit fullscreen mode

It should works, but how ?

Behind the scene

Image description

When we make the request, it flows through a filter chain with differents filters, when a filter detects the headers he wants, he can call the AuthenticationManager. In our case, when the BasicAuthenticationFilter receive a request with username and password, it call the AuthenticationManager with a non valided UsernamePasswordToken.

The AuthenticationManager will then find a provider that handle this token, in our case the DaoAuthenticationProvider. It will use the UserDetails to find User with the same username as the credentials, then encode the password and compare it with the credentials password. If it matchs, it will update the token, add it to the SecurityContext, and return the token to the manager, then to the filter.


You can find the source code here

Top comments (0)