In this article of build REST API with Spring, we learn how to Secure a REST API using Spring Security with token based authentication. We will see the steps to secure a REST API with Spring Security and Spring Boot.
Introduction
In our previous article we saw how to build a basic authentication with Spring Security for REST API. Basic authentication has a certain limitation and it might not fit in to all use cases. We will extend this article to see how to implement a token bases security feature with Spring. Let’s look at the workflow for a better understanding:
- User send a request with a username and password.
- Spring security return token back to client API.
- Client API sends token in each request as part of authentication.
- Token invalidated on log out.
Let’s see how this workflow looks like:
1. Maven Setup
We will use Spring Boot and Maven to handle the dependencies. As we are building the Spring Boot web application, we will use following staters for our application.
- Spring Boot Web starter
- Spring Boot Security starter.
- JPA starter
This is now our pom.xml looks like:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2. Database layout
I am keeping this application simple at the database level, I will use a single table to store user details and token. There will be no token against user profile till they request application to create one and return this token. This is how the table structure look like:
This is not a production ready table, but the main idea is to store the token for the customer profile and use this token for authentication and authorization. You can change / adapt this workflow based on your requirement.
3. JPA Repository
To save and get the token information for customer profile, we need to create a custom repository. This repository is responsible to get customer information based on the token. Customer service will use our customer repository to get the customer details based on the token or to perform the login.
@Repository
public interface CustomerRepository extends CrudRepository<Customer, Long> {
@Query(value = "SELECT u FROM Customer u where u.userName = ?1 and u.password = ?2 ")
Optional login(String username,String password);
Optional findByToken(String token);
}
4. Customer Validation Service
Our customer validation service follows two core operations
- Provide login feature to return token to the client.
- Validate customer based on the provided token.
This is how our customer service looks like:
@Service("customerService")
public class DefaultCustomerService implements CustomerService {
@Autowired
CustomerRepository customerRepository;
@Override
public String login(String username, String password) {
Optional customer = customerRepository.login(username,password);
if(customer.isPresent()){
String token = UUID.randomUUID().toString();
Customer custom= customer.get();
custom.setToken(token);
customerRepository.save(custom);
return token;
}
return StringUtils.EMPTY;
}
@Override
public Optional findByToken(String token) {
Optional customer= customerRepository.findByToken(token);
if(customer.isPresent()){
Customer customer1 = customer.get();
User user= new User(customer1.getUserName(), customer1.getPassword(), true, true, true, true,
AuthorityUtils.createAuthorityList("USER"));
return Optional.of(user);
}
return Optional.empty();
}
}
Let’s inspect what we are doing in the above code:
- Login method accepts the user name and password and will return a token for successful credential.
- We will use the second method for all secured resources
5. Spring Security Configurations
These are the main configuration classes to secure a REST API using Spring Security with token based authentication.In this section, we will talk about following classes:
AuthenticationProvider
: Find the user by its authentication token.AuthenticationFilter
:Extract the authentication token from the request headersSecurityConfiguration
: Spring Security Configuration
5.1 Token Authentication Provider
The AuthenticationProvider
is responsible to find user based on the authentication token sent by the client in the header. This is how our Spring based token authentication provider looks like:
@Component
public class AuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Autowired
CustomerService customerService;
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
//
}
@Override
protected UserDetails retrieveUser(String userName, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
Object token = usernamePasswordAuthenticationToken.getCredentials();
return Optional
.ofNullable(token)
.map(String::valueOf)
.flatMap(customerService::findByToken)
.orElseThrow(() -> new UsernameNotFoundException("Cannot find user with authentication token=" + token));
}
Our AuthenticationProvider
use the CustomerService
to find a customer based on the token.
5.2 Token Authentication Filter
The token authentication filter is responsible to get the authentication filter from the header and call the authentication manager for authentication. This is how the authentication filter looks like:
public class AuthenticationFilter extends AbstractAuthenticationProcessingFilter {
AuthenticationFilter(final RequestMatcher requiresAuth) {
super(requiresAuth);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
Optional tokenParam = Optional.ofNullable(httpServletRequest.getHeader(AUTHORIZATION)); //Authorization: Bearer TOKEN
String token= httpServletRequest.getHeader(AUTHORIZATION);
token= StringUtils.removeStart(token, "Bearer").trim();
Authentication requestAuthentication = new UsernamePasswordAuthenticationToken(token, token);
return getAuthenticationManager().authenticate(requestAuthentication);
}
@Override
protected void successfulAuthentication(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain, final Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
Let’s highlight few important points in this:
- This filter delegates Authentication to the
- This filter is only enable for specific URLS (explained in next section)
5.3 Spring Security Configurations
This is responsible to club everything together.Let’s see how our Spring security configuration looks like:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PROTECTED_URLS = new OrRequestMatcher(
new AntPathRequestMatcher("/api/**")
);
AuthenticationProvider provider;
public SecurityConfiguration(final AuthenticationProvider authenticationProvider) {
super();
this.provider = authenticationProvider;
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) {
auth.authenticationProvider(provider);
}
@Override
public void configure(final WebSecurity webSecurity) {
webSecurity.ignoring().antMatchers("/token/**");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.and()
.authenticationProvider(provider)
.addFilterBefore(authenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.requestMatchers(PROTECTED_URLS)
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
}
@Bean
AuthenticationFilter authenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationManager(authenticationManager());
//filter.setAuthenticationSuccessHandler(successHandler());
return filter;
}
@Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(HttpStatus.FORBIDDEN);
}
}
Let’s inspect some important points:
- All the URL matching with request pattern
/api/**
are secure and need a valid token for the access. - The
webSecurity.ignoring().antMatchers("/token/**")
shows all requests excluded from the security check. - We have registered the
AuthenticationProvider
with the Spring security. Spring security will it to check token validation. - The configure method includes basic configuration along with disabling the form based login and other standard features
This step concludes the steps to secure a REST API using Spring Security with token based authentication. In the next step, we will setup a simple Spring Boot web application to test our workflow.
6. Spring Boot Controller
Let’s create a simple Spring Boot controller to test our application:
6.1 Token Controller
This controller is responsible to return a token for valid credentials:
@RestController
public class TokenController {
@Autowired
private CustomerService customerService;
@PostMapping("/token")
public String getToken(@RequestParam("username") final String username, @RequestParam("password") final String password){
String token= customerService.login(username,password);
if(StringUtils.isEmpty(token)){
return "no token found";
}
return token;
}
}
6.2 Secure User Profile Controller
This is the secure controller. It will return user profile for a valid token.This controller is only accessible on passing a valid token:
@RestController
public class UserProfileController {
@Autowired
private CustomerService customerService;
@GetMapping(value = "/api/users/user/{id}",produces = "application/json")
public Customer getUserDetail(@PathVariable Long id){
return customerService.findById(id);
}
}
7. Testing Application
Let’s build and deploy our application.Once the application is running, let’s use any REST client to test our application (I am using Postman):
Without Access Token:
Let’s get a token from the API:
Use the Token for the secure URL’s
Summary
In this article, we saw how to use token based approach to secure a REST API using Spring Security. We covered the different configurations and setup to secure our REST API. The source code for this post is available on the GitHub.