DEV Community

Cover image for Securing your rest API with SpringSecurity
Erwan Le Tutour
Erwan Le Tutour

Posted on

1 1 1 1 1

Securing your rest API with SpringSecurity

order66

What we will do

After creating our API in the previous step, we will now secure it using Spring Security.
In order to do so, we need to add 2 dependencies to our pom.xml file

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

What it will look like

UML

Execute Order 66

The Account Entity

In order to secure our API, we will use some roles, so to achieve that, we will create an account entity that will use those roles.

package com.erwan.human.domaine;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Getter
@Setter
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private String password;
private String role;
public Account(String username, String password, String role) {
this.username = username;
this.password = password;
this.role = role;
}
public Account(){}
@Override
public String toString() {
return "Account {" +
"id=" + id +
", username='" + username + '\'' +
", role='" + role + '\'' +
'}';
}
}
view raw Account.java hosted with ❤ by GitHub

The Repository

In order to know if the user who will try to use our API exists and have the role associated we also need to create the repository linked to the Account entity.

package com.erwan.human.dao;
import com.erwan.human.domaine.Account;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AccountRepository extends JpaRepository<Account, Long> {
Account findOneByUsername(String username);
}

When we created the CloneRepository earlier, we didn’t need to create any method in it because all the methods we used were already implemented thanks to inheritance.
Here we will need a specific method like the one above findOneByUsername, with that one our repository will know that we search only one result that matches the String username passed on the argument. (see Spring Data JPA doc if you want to know how it works).

The controller

Now that we have created the account that will have the roles to connect to our API we can update our controller to make it accept these roles on its method.

package com.erwan.human.controller;
import com.erwan.human.dao.CloneRepository;
import com.erwan.human.domaine.Clone;
import com.erwan.human.exceptions.BeanNotFound;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.SortDefault;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/kamino/clone")
public class HumanCloningController {
@Autowired
private CloneRepository repository;
@GetMapping("/")
@PreAuthorize("hasAnyAuthority('ROLE_KAMINOAIN', 'ROLE_EMPEROR')")
public List<Clone> findAll() {
return repository.findAll();
}
@GetMapping("/pages")
@PreAuthorize("hasAnyAuthority('ROLE_KAMINOAIN', 'ROLE_EMPEROR')")
public Page<Clone> findAllPages(@PageableDefault(page = 0, size = 20)
@SortDefault.SortDefaults({
@SortDefault(sort = "id", direction = Sort.Direction.ASC)
}) Pageable pageable) {
return repository.findAll(pageable);
}
@PostMapping()
@PreAuthorize("hasAnyAuthority('ROLE_KAMINOAIN', 'ROLE_EMPEROR')")
public Clone createClone(@RequestBody Clone clone){
return repository.save(clone);
}
@GetMapping("/{id}")
@PreAuthorize("hasAnyAuthority('ROLE_KAMINOAIN', 'ROLE_EMPEROR')")
public Clone findById(@PathVariable("id") Long id) throws BeanNotFound {
return getOne(id);
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyAuthority('ROLE_KAMINOAIN', 'ROLE_EMPEROR')")
public void delete(@PathVariable("id") Long id) throws BeanNotFound {
Clone clone = getOne(id);
repository.delete(clone);
}
@PutMapping("/order66")
@PreAuthorize("hasAuthority('ROLE_EMPEROR')")
public List<Clone> executeOrder66(){
List<Clone> clones = repository.findAll();
clones.stream().forEach(clone -> clone.setAffiliation("Galactic Empire"));
return repository.saveAll(clones);
}
protected Clone getOne(Long id) throws BeanNotFound {
Optional<Clone> clone = repository.findById(id);
if(!clone.isPresent()){
throw new BeanNotFound("Can't find clone with id : " + id);
}
return clone.get();
}
}

As you can see, our method now avec the @PreAuthorize annotation that indicates the roles who can access it.

So, is my API secured now? The response is NO !
We have created the account entity and its a repository, we have upgraded our controller to indicate the roles that it will use, but we have yet to implement the security configuration.
So now let’s implement it.

The configuration

In order to make all the above code run properly, we will implement security.
To do so we will extend WebSecurityConfigurerAdapter in a configuration class.

Configuration

package com.erwan.human.config;
import com.erwan.human.services.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationService authenticationService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/kamino/**").hasAnyRole("KAMINOAIN", "EMPEROR")
.and()
.authorizeRequests().antMatchers("/console/**").permitAll()// pour continuer à avoir accès à la console de la bdd
.and()
.httpBasic();
http.csrf().disable().cors().disable();
http.headers().frameOptions().disable();
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth
.eraseCredentials(true)
.userDetailsService(authenticationService)
.passwordEncoder(passwordEncoder());
}
}

As you can see in the first configure method, we indicate the roles who can access the endpoint.
In the second method, we describe the service that will be used to authenticate the user.

Authentication Service

To authenticate our user we need to create a service that will implement the UserDetailsService from the Spring Security package, especially the method loadUserByUsername.

package com.erwan.human.services;
import com.erwan.human.dao.AccountRepository;
import com.erwan.human.domaine.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
@Service
public class AuthenticationService implements UserDetailsService {
@Autowired
private AccountRepository accountRepository;
@Override
public User loadUserByUsername(String userName) throws UsernameNotFoundException {
Account u = accountRepository.findOneByUsername(userName);
System.out.println(u.toString());
if(u == null){
throw new UsernameNotFoundException("Utilisateur non trouvé : " + userName);
}
User user = createUser(u);
return user;
}
private User createUser(Account u) {
return new User(u.getUsername(), u.getPassword(), createAuthorities(u));
}
private Collection<GrantedAuthority> createAuthorities(Account u) {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_"+u.getRole()));
return authorities;
}
}

Testing our API

To test our API we need to first launch our application.
Then we can try to connect to the entry point that we created in our controller using postman or any other tool that permits you to make HTTP calls.

the examples will be the same as when we created the API, but with basic authentication.
In order to test my API, I have created 2 accounts that will be pre-loaded in the database.

package com.erwan.human.services;
import com.erwan.human.dao.AccountRepository;
import com.erwan.human.domaine.Account;
import com.erwan.human.reference.AccountRole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.transaction.Transactional;
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@PostConstruct
public void initialize(){
if(accountRepository.findOneByUsername("kamino") == null){
save(new Account("kamino", "kamino", AccountRole.KAMINOAIN.name()));
}
if(accountRepository.findOneByUsername("palpatine") == null){
save(new Account("palpatine", "palpatine", AccountRole.EMPEROR.name()));
}
}
@Transactional
private Account save(Account user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
return accountRepository.save(user);
}
}

So now if I try to access a datapoint with bad credentials, i will have a 401 error.
{
  "timestamp": "2021-03-08T09:45:09.332+00:00",
  "status": 401,
  "error": "Unauthorized",
  "message": "Unauthorized",
  "path": "/kamino/order66"
}
Enter fullscreen mode Exit fullscreen mode

And if i try to access a datapoint when i don’t have the appropriate role, i will have a 403 error.

postman

I have intercepted my 403 error in my exceptions handler to have something more readable than the exception stack trace.

Thanks for your reading time, as previously, the code used in this tutorial is findable in this Github repository, branch security.

Top comments (0)