Programming, Security

Setup HTTP Basic Authentication with your Spring Boot API

Spring is one of the most (if not the most) used Java frameworks. It has many great features and can allow you to quickly bootstrap a project, but if you're building an API, the authentication management part is not well documented. In this article we will discuss how you can take advantage of Spring Security (v5.7.3) to secure your Spring Boot API and fine-tune permissions with roles. In what follows, I will assume that you have some knowlege of Spring and Hibernate.

Authentication Options

Sadly, Spring Security doesn't provide anything to generate Tokens and use them as an authentication mechanism (or at least it is not visible in the documentation,) so we will use Basic Auth.

Previously, the Authentication could be managed with the class WebSecurityConfigurerAdapter (almost all of the tutorials on the Internet are using it,) but it has been deprecated this year, so we will not use it.

Let's Code

By default, Spring Security provides users and roles classes, but they seem to require applying some database patches and are not flexible. Instead of using them, we will create our own classes and implement the right interfaces, so that it will be easier now, and in the future if we want to extend the functionalities of our User class. Note that I will use Lombok and JPA in the following code.

Users

To make our User class compatible with Spring Security, we will need to implement org.springframework.security.core.userdetails.UserDetails. At a minimum, you will need to have a username and password attributes, which are used by the framework. You will also need to implement various methods such as getAuthorities (we will get to this one in more detail), isLocked, ... These methods will be used by Spring Security to grant access, so be careful about what you make them return.

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

    @Id @GeneratedValue @Getter
    private Long id;

    @Getter @Setter
    private String email;

    @Getter @Setter
    @Column(unique = true)
    private String username;

    @Getter @Setter
    private String password;

    @Getter @Setter
    private boolean enabled;

    @Getter @Setter
    private boolean locked;

    @Getter @Setter
    @Column(name = "expiration_date")
    private Date expirationDate;

    @Setter @Getter
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
    private Set<UserRole> roles = new HashSet<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles;
    }

    @Override
    public boolean isAccountNonExpired() {
        return expirationDate == null || expirationDate.after(new Date());
    }

    @Override
    public boolean isAccountNonLocked() {
        return !isLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
}
Users.java

You will note the @Audited annotation. This is provided by org.hibernate:hibernate-envers. What it does is making sure to keep old versions of the object every time a change is pushed to the database, which is nice to have when it comes to users management.

Roles Implementation

Now that we have our basic User class, let's configure the roles. The implementation is pretty trivial, as you only need to implement GrantedAuthority and override the method getAuthority.

Here I defined two roles using an enum, but nothing prevents you from doing things differently and using different roles. You should just make sure to name them as ROLE_[...].

@Embeddable
public class UserRole implements GrantedAuthority {

    @Setter
    @Enumerated(EnumType.STRING)
    @Column(name = "role_name")
    private RoleName roleName;

    @Override
    public String getAuthority() {
        return roleName.name();
    }

    public enum RoleName {
        ROLE_ADMIN,
        ROLE_USER
    }
}
UserRole.java

Database Access

The next step is to make sure that Spring Security can get users' details from the database. For this, we will need to implement two classes. First, a JpaRepository to allow querying the database.

@Configuration
public interface UserRepository extends JpaRepository<User, Long> {

    User findUserByUsername(String username);
}
UserRepository.java

Then, a class implementing UserDetailsService, which is what Spring Security is wired to use to find users.

@Component
public class SecurityUserDetailService implements UserDetailsService {

    private final UserRepository _userRepository;

    public SecurityUserDetailService(UserRepository userRepository) {
        this._userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return _userRepository.findUserByUsername(username);
    }
}
SecurityUserDetailService

Security Configuration

Finally, we need to create a configuration class to define how Spring Security will perform. Note that we are disabling the CSRF protection, and configuring the session management to be stateless. This is because we are writing this to be used with a REST API. If you are building a WebUI, you should not do that.

Also, note the passwordEncoder method which is used to define how the passwords are hashed. Here, they are using 10 rounds of bcrypt.

@Configuration
@EnableMethodSecurity
public class SecurityConfiguration
{
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
        return http.build();
    }

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

    @Bean
    public AuthenticationManager authManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, SecurityUserDetailService securityUserDetailService) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(securityUserDetailService)
                .passwordEncoder(bCryptPasswordEncoder)
                .and()
                .build();
    }
}
SecurityConfiguration

Allow Methods to be Called by Specific Roles

If you applied the configuration until here,  your application now requires users to be authenticated to access all of your endpoints. Now, let's look at how to do some fine tuning on who can access what.

Firstly, you can manage things from the configuration class. For example, you can change the filterChain method as follows to allow anyone to query /api/public without being authenticated, and require to have the role ADMIN to access /api/admin

http.csrf().disable()
	.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and().authorizeRequests()
        .antMatchers("/api/public").permitAll()
        .antMatchers("/api/admin").hasRole("ADMIN")
	.anyRequest().authenticated().and().httpBasic();

One other option that you have is to annotate your controllers directly. Here, we require users to have the role ADMIN to access /api/admin

@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
	...

Finally, you can directly annotate methods in your controllers. You will notice that the method uses @AuthenticationPrincipal. This allows getting the authenticated user doing the request and is totally optional.

@GetMapping("/user/{userId}")
@PreAuthorize("hasRole('ADMIN')")
ResponseEntity<?> getUser(@AuthenticationPrincipal User user, @PathVariable Long userId) {
	...

Final Words

In this article, we learned how to implement basic authentication mechanisms to provide authentication and authorization thanks to Spring-Security when writing a REST API with Spring Boot. This should be enough to get you started, but note that the features of Spring Security are not limited to what is listed in this article. For example, you can authorize multiple groups, use other anotations such as @PostAuthorize, ...

Sources and Credits

Sources

Credits

Author image

About Ixonae

You've successfully subscribed to Ixonae on Security
Great! Next, complete checkout for full access to Ixonae on Security
Welcome back! You've successfully signed in.
Unable to sign you in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.