Securing a Rest API with Spring Security
Most Spring Tutorials available online teach you how to secure a Rest API with Spring with examples which are far from real application problematics. You surely agree that most tutorials lack real-world use-cases.
This tutorial aims to help you secure a real-world application, not just another Hello World Example.
In this tutorial we'll learn:
- How to secure a Spring MVC Rest API using Spring Security,
- Configure Spring Security with Java code (no painful XML),
- And delegate authentication to a UserAuthenticationService with your own business logic.
I've spent several weeks tweaking Spring Security to come up with this simple setup. Let's go!
Complete Source code is available on Github.
Architecture¶
The following Spring security setup works as following:
- The user logs in with a POST request containing his username and password,
- The server returns a temporary / permanent authentication token,
- The user sends the token within each HTTP request via an HTTP header
Authorization: Bearer TOKEN
.
When the user logs out, the token is cleared on server-side. That's it!
Now, let's see different examples with variety of authentications:
- Simple Example: authentication based on the
UUID
of the user, - JWT Example: authentication based on a JWT token.
Let's now briefly see how the maven modules are organized. Implementing modules only depends on API modules. It's up to the application module (like example-simple
) to tie the implementations together.
Simple Example¶
The architecture diagram above shows how the example-simple
module interacts with the other modules.
JWT Example¶
The main difference with the example-simple
module are the dependencies on user-auth-token
and token-jwt
modules.
Now that we have an overview of the overall architecture, let's dive into the code!
Maven POM¶
Let's setup the root maven module with the following configuration:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
</parent>
<groupId>com.octoperf</groupId>
<artifactId>securing-rest-api-spring-security</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>example-simple</module>
<module>example-jwt</module>
<module>user-entity</module>
<module>user-auth-api</module>
<module>user-crud-api</module>
<module>user-crud-in-memory</module>
<module>user-controller</module>
<module>user-auth-uuid</module>
<module>security-config</module>
<module>user-auth-token</module>
<module>token-api</module>
<module>token-jwt</module>
<module>date-service</module>
<module>bootstrap</module>
</modules>
<properties>
<commons-lang.version>3.8.1</commons-lang.version>
<guava.version>29.0-jre</guava.version>
<jaxb.version>2.3.3</jaxb.version>
<jwt.version>0.9.1</jwt.version>
<joda-time.version>2.10.6</joda-time.version>
<junit.version>4.12</junit.version>
<mockito.version>3.2.4</mockito.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${joda-time.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-testlib</artifactId>
<version>${guava.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
<!-- mandatory to compile project with maven 3.3.9, might be removed with latest version -->
<useIncrementalCompilation>false</useIncrementalCompilation>
<optimize>true</optimize>
</configuration>
</plugin>
</plugins>
</build>
</project>
In this example, we're going to use Spring Boot 2.3 to quickly setup a web application using Spring MVC and Spring Security.
Common Configuration¶
User Management¶
In this section, i'm going to cover the implementation of the code responsible of logging in and out users.
user-entity¶
The user-entity
module contains User
class which represents a single user:
package com.octoperf.user.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import static java.util.Objects.requireNonNull;
@Value
@Builder
public class User implements UserDetails {
private static final long serialVersionUID = 2396654715019746670L;
String id;
String username;
String password;
@JsonCreator
User(@JsonProperty("id") final String id,
@JsonProperty("username") final String username,
@JsonProperty("password") final String password) {
super();
this.id = requireNonNull(id);
this.username = requireNonNull(username);
this.password = requireNonNull(password);
}
@JsonIgnore
@Override
public Collection<GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
To match Spring Security API, the User
class implements UserDetails
. This way, our custom User
bean seamlessly integrates into Spring Security.
user-crud-api¶
The User crud API is responsible of storing users somewhere.
package com.octoperf.user.crud.api;
import com.octoperf.user.entity.User;
import java.util.Optional;
/**
* User security operations like login and logout, and CRUD operations on {@link User}.
*
* @author jerome
*
*/
public interface UserCrudService {
User save(User user);
Optional<User> find(String id);
Optional<User> findByUsername(String username);
}
The unique implementation is InMemoryUsers
located in module user-crud-in-memory
:
package com.octoperf.user.crud.in.memory;
import com.octoperf.user.crud.api.UserCrudService;
import com.octoperf.user.entity.User;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import static java.util.Optional.ofNullable;
@Service
final class InMemoryUsers implements UserCrudService {
Map<String, User> users = new HashMap<>();
@Override
public User save(final User user) {
return users.put(user.getId(), user);
}
@Override
public Optional<User> find(final String id) {
return ofNullable(users.get(id));
}
@Override
public Optional<User> findByUsername(final String username) {
return users
.values()
.stream()
.filter(u -> Objects.equals(username, u.getUsername()))
.findFirst();
}
}
As you can see, users are stored in Map<String, User>
in memory. This is purely for demonstration purpose. Of course, a real application would be based on a UserCrudService
storing users in a real database.
user-auth-api¶
The user-auth-api
contains UserAuthenticationService
is responsible of logging in and out the users, as well as deliver the authentication tokens.
package com.octoperf.auth.api;
import com.octoperf.user.entity.User;
import java.util.Optional;
public interface UserAuthenticationService {
/**
* Logs in with the given {@code username} and {@code password}.
*
* @param username
* @param password
* @return an {@link Optional} of a user when login succeeds
*/
Optional<String> login(String username, String password);
/**
* Finds a user by its dao-key.
*
* @param token user dao key
* @return
*/
Optional<User> findByToken(String token);
/**
* Logs out the given input {@code user}.
*
* @param user the user to logout
*/
void logout(User user);
}
In this tutorial, i'm going to use 2 different implementations depending on the example we'll see.
Spring Security Config¶
The whole Spring Security configuration is stored in security-config
module.
Redirect Strategy¶
As we're securing a REST API, in case of authentication failure, the server should not redirect to any error page. The server will simply return an HTTP 401 (Unauthorized). Here is the NoRedirectStrategy
located in com.octoperf.security
package:
package com.octoperf.security.config;
import org.springframework.security.web.RedirectStrategy;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
class NoRedirectStrategy implements RedirectStrategy {
@Override
public void sendRedirect(final HttpServletRequest request, final HttpServletResponse response, final String url) throws IOException {
// No redirect is required with pure REST
}
}
Nothing fancy here, the purpose is to keep things simple.
Token Authentication Provider¶
The TokenAuthenticationProvider
is responsible of finding the user by it's authentication token.
package com.octoperf.security.config;
import com.octoperf.auth.api.UserAuthenticationService;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Optional;
import static lombok.AccessLevel.PACKAGE;
import static lombok.AccessLevel.PRIVATE;
@Component
@AllArgsConstructor(access = PACKAGE)
@FieldDefaults(level = PRIVATE, makeFinal = true)
final class TokenAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@NonNull
UserAuthenticationService auth;
@Override
protected void additionalAuthenticationChecks(final UserDetails d, final UsernamePasswordAuthenticationToken auth) {
// Nothing to do
}
@Override
protected UserDetails retrieveUser(final String username, final UsernamePasswordAuthenticationToken authentication) {
final Object token = authentication.getCredentials();
return Optional
.ofNullable(token)
.map(String::valueOf)
.flatMap(auth::findByToken)
.orElseThrow(() -> new UsernameNotFoundException("Cannot find user with authentication token=" + token));
}
}
The TokenAuthenticationProvider
delegates to the UserAuthenticationService
we have seen in the previous section.
TokenAuthenticationFilter¶
The TokenAuthenticationFilter
is responsible of extracting the authentication token from the request headers. It takes the Authorization
header value and attempts to extract the token from it.
package com.octoperf.security.config;
import lombok.experimental.FieldDefaults;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static java.util.Optional.ofNullable;
import static lombok.AccessLevel.PRIVATE;
import static org.apache.commons.lang3.StringUtils.removeStart;
@FieldDefaults(level = PRIVATE, makeFinal = true)
final class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String BEARER = "Bearer";
TokenAuthenticationFilter(final RequestMatcher requiresAuth) {
super(requiresAuth);
}
@Override
public Authentication attemptAuthentication(
final HttpServletRequest request,
final HttpServletResponse response) {
final String param = ofNullable(request.getHeader(AUTHORIZATION))
.orElse(request.getParameter("t"));
final String token = ofNullable(param)
.map(value -> removeStart(value, BEARER))
.map(String::trim)
.orElseThrow(() -> new BadCredentialsException("Missing Authentication Token"));
final Authentication auth = new UsernamePasswordAuthenticationToken(token, token);
return getAuthenticationManager().authenticate(auth);
}
@Override
protected void successfulAuthentication(
final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain chain,
final Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
}
Again, nothing fancy here. The code is pretty straight forward! Authentication is then delegated to the AuthenticationManager
. The filter is only enabled for a given set of urls. We are going to see in the next coming sections how this filter is configured.
SecurityConfig¶
It's time to configure Spring Security with all the services we defined above:
package com.octoperf.security.config;
import lombok.experimental.FieldDefaults;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import static java.util.Objects.requireNonNull;
import static lombok.AccessLevel.PRIVATE;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
@FieldDefaults(level = PRIVATE, makeFinal = true)
class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
new AntPathRequestMatcher("/public/**")
);
private static final RequestMatcher PROTECTED_URLS = new NegatedRequestMatcher(PUBLIC_URLS);
TokenAuthenticationProvider provider;
SecurityConfig(final TokenAuthenticationProvider provider) {
super();
this.provider = requireNonNull(provider);
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) {
auth.authenticationProvider(provider);
}
@Override
public void configure(final WebSecurity web) {
web.ignoring().requestMatchers(PUBLIC_URLS);
}
@Override
protected void configure(final HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(STATELESS)
.and()
.exceptionHandling()
// this entry point handles when you request a protected page and you are not yet
// authenticated
.defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
.and()
.authenticationProvider(provider)
.addFilterBefore(restAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.requestMatchers(PROTECTED_URLS)
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable();
}
@Bean
TokenAuthenticationFilter restAuthenticationFilter() throws Exception {
final TokenAuthenticationFilter filter = new TokenAuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(successHandler());
return filter;
}
@Bean
SimpleUrlAuthenticationSuccessHandler successHandler() {
final SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
successHandler.setRedirectStrategy(new NoRedirectStrategy());
return successHandler;
}
/**
* Disable Spring boot automatic filter registration.
*/
@Bean
FilterRegistrationBean disableAutoRegistration(final TokenAuthenticationFilter filter) {
final FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
@Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(FORBIDDEN);
}
}
Let's review how Spring Security is configured here:
- URLs starting with
/public/**
are excluded from security, which means any url starting with/public
will not be secured, - The
TokenAuthenticationFilter
is registered within the Spring Security Filter Chain very early. We want it to catch any authentication token passing by, - Most other login methods like
formLogin
orhttpBasic
have been disabled as we're not willing to use them here (we want to use our own system), - Some boiler-plate code to disable automatic filter registration, related to Spring Boot.
As you can see, everything is tied together in a Java configuration which is almost less than 100 lines everything combined!
Now, we're going to setup a few Spring MVC RestController
to be able to login and logout.
Spring MVC Controllers¶
Those controllers are shared by all the examples we'll see below.
PublicUsersController¶
The PublicUsersController
allows a user to login into the application:
package com.octoperf.user.controller;
import com.octoperf.auth.api.UserAuthenticationService;
import com.octoperf.user.crud.api.UserCrudService;
import com.octoperf.user.entity.User;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import static lombok.AccessLevel.PACKAGE;
import static lombok.AccessLevel.PRIVATE;
@RestController
@RequestMapping("/public/users")
@FieldDefaults(level = PRIVATE, makeFinal = true)
@AllArgsConstructor(access = PACKAGE)
final class PublicUsersController {
@NonNull
UserAuthenticationService authentication;
@NonNull
UserCrudService users;
@PostMapping("/register")
String register(
@RequestParam("username") final String username,
@RequestParam("password") final String password) {
users
.save(
User
.builder()
.id(username)
.username(username)
.password(password)
.build()
);
return login(username, password);
}
@PostMapping("/login")
String login(
@RequestParam("username") final String username,
@RequestParam("password") final String password) {
return authentication
.login(username, password)
.orElseThrow(() -> new RuntimeException("invalid login and/or password"));
}
}
It offers 2 different Endpoints:
String register(@RequestParam("username") final String username, @RequestParam("password") final String password)
: Register a new user and return an authentication token,String login(@RequestParam("username") final String username, @RequestParam("password") final String password)
: login an existing user and return an authentication token (if any user found with matching password).
The authentication is delegated to UserAuthenticationService
implementation. UserCrudService
is responsible of storing the user.
SecuredUsersController¶
The SecuredUsersController
is by definition permitting the user to perform operations only when logged in:
- Get the current user bean,
- Logout from the application.
Here is the code:
package com.octoperf.user.controller;
import com.octoperf.auth.api.UserAuthenticationService;
import com.octoperf.user.entity.User;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static lombok.AccessLevel.PACKAGE;
import static lombok.AccessLevel.PRIVATE;
@RestController
@RequestMapping("/users")
@FieldDefaults(level = PRIVATE, makeFinal = true)
@AllArgsConstructor(access = PACKAGE)
final class SecuredUsersController {
@NonNull
UserAuthenticationService authentication;
@GetMapping("/current")
User getCurrent(@AuthenticationPrincipal final User user) {
return user;
}
@GetMapping("/logout")
boolean logout(@AuthenticationPrincipal final User user) {
authentication.logout(user);
return true;
}
}
Again, nothing difficult here! It's time now to test the application. To do so, we need to create a Spring Boot bootstrap class.
Application Bootstrap¶
The Application
class placed in root package com.octoperf
in maven module bootstrap
is responsible for bootstrapping the application via Spring Boot:
package com.octoperf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args); // NOSONAR
}
}
We're going to run this application like a simple Java main to launch the server. Finally, the server runs on port 8080
by default. As I already have a server running on this port on my machine. As a result, I configured Spring Boot to run on port 8081
via an application.yml
located in bootstrap module too
:
server.port: 8081
Simple Example¶
user-auth-uuid¶
This module contains a UserAuthenticationService
which is based on a simple random UUID.
package com.octoperf.user.auth.map;
import com.octoperf.auth.api.UserAuthenticationService;
import com.octoperf.user.crud.api.UserCrudService;
import com.octoperf.user.entity.User;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.UUID;
import static lombok.AccessLevel.PACKAGE;
import static lombok.AccessLevel.PRIVATE;
@Service
@AllArgsConstructor(access = PACKAGE)
@FieldDefaults(level = PRIVATE, makeFinal = true)
final class UUIDAuthenticationService implements UserAuthenticationService {
@NonNull
UserCrudService users;
@Override
public Optional<String> login(final String username, final String password) {
final String uuid = UUID.randomUUID().toString();
final User user = User
.builder()
.id(uuid)
.username(username)
.password(password)
.build();
users.save(user);
return Optional.of(uuid);
}
@Override
public Optional<User> findByToken(final String token) {
return users.find(token);
}
@Override
public void logout(final User user) {
}
}
The service logs in any user. I've said it, it's pretty simple! You can plug here your own authentication logic instead of this dummy one. It's up to you to adapt the code to your own needs.
Configuration¶
The example-simple
module is an example application which has the following configuration:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<parent>
<groupId>com.octoperf</groupId>
<artifactId>securing-rest-api-spring-security</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.octoperf</groupId>
<artifactId>example-simple</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>user-auth-uuid</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>user-controller</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>user-crud-in-memory</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>bootstrap</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>security-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
It features a simple example based on the UUID authentication token: the id of the user is used as authentication token.
Create a launcher (within Intellij) with following configuration:
- Main Class:
com.octoperf.Application
, - Working Directory:
$MODULE_DIR$
, - Use classpath of module:
example-simple
.
Then run it. The application should be running within a few seconds:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.3.RELEASE)
2020-08-28 10:37:58.821 INFO 18654 --- [ main] com.octoperf.Application : Starting Application on t440p with PID 18654 (/home/ubuntu/git/securing-rest-api-spring-security/bootstrap/target/classes started by ubuntu in /home/ubuntu/git/securing-rest-api-spring-security/bootstrap)
2020-08-28 10:37:58.831 INFO 18654 --- [ main] com.octoperf.Application : No active profile set, falling back to default profiles: default
2020-08-28 10:38:04.150 INFO 18654 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http)
2020-08-28 10:38:04.175 INFO 18654 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-08-28 10:38:04.175 INFO 18654 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.37]
2020-08-28 10:38:04.485 INFO 18654 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-08-28 10:38:04.485 INFO 18654 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 5358 ms
2020-08-28 10:38:05.252 INFO 18654 --- [ main] o.s.boot.web.servlet.RegistrationBean : Filter tokenAuthenticationFilter was not registered (disabled)
2020-08-28 10:38:05.737 INFO 18654 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: OrRequestMatcher [requestMatchers=[Ant [pattern='/public/**'], Ant [pattern='/error/**']]], []
2020-08-28 10:38:05.842 INFO 18654 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@3a082ff4, org.springframework.security.web.context.SecurityContextPersistenceFilter@434514d8, org.springframework.security.web.header.HeaderWriterFilter@6342d610, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4613311f, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@3dfa819, com.octoperf.security.config.TokenAuthenticationFilter@6ba7383d, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@45acdd11, org.springframework.security.web.session.SessionManagementFilter@784abd3e, org.springframework.security.web.access.ExceptionTranslationFilter@53f4c1e6, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@242b6e1a]
2020-08-28 10:38:06.574 INFO 18654 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-08-28 10:38:07.628 INFO 18654 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
2020-08-28 10:38:07.682 INFO 18654 --- [ main] com.octoperf.Application : Started Application in 11.987 seconds (JVM running for 14.617)
2020-08-28 10:38:48.659 INFO 18654 --- [nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-08-28 10:38:48.660 INFO 18654 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-08-28 10:38:48.681 INFO 18654 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 21 ms
Great! The server is up and running, ready to be used. Let's now perform some requests using curl
.
Testing the Application¶
First, let's register on the REST API:
ubuntu@laptop:~$ curl -XPOST -d 'username=john&password=smith' http://localhost:8081/public/users/register
b856850e-1ad4-456d-b5ca-1c2bfc355e5
Then we can also login with this username and password:
ubuntu@laptop:~$ curl -XPOST -d 'username=john&password=smith' http://localhost:8081/public/users/login
b856850e-1ad4-456d-b5ca-1c2bfc355e5
By sending an url-encoded form post request to the endpoint, it returns as expected a random UUID
. Now, let's use the UUID in a subsequent request to retrieve the current user:
ubuntu@laptop:~$ curl -H 'Authorization: Bearer b856850e-1ad4-456d-b5ca-1c2bfc355e5e' http://localhost:8081/users/current
{"id":"b856850e-1ad4-456d-b5ca-1c2bfc355e5e","username":"john","enabled":true}
Nice! We're logged into the system and we could retrieve the current user in Json format. By default, Spring Boot uses Jackson Json API to serialize beans into Json.
Let's now logout from the system:
ubuntu@laptop:~$ curl -H 'Authorization: Bearer b856850e-1ad4-456d-b5ca-1c2bfc355e5e' http://localhost:8081/users/logout
true
If we try to get the current user again with the same authentication token, we should receive an error:
ubuntu@laptop:~$ curl -H 'Authorization: Bearer b856850e-1ad4-456d-b5ca-1c2bfc355e5e' http://localhost:8081/users/current
{"timestamp":1516184750678,"status":401,"error":"Unauthorized","message":"Authentication Failed: Bad credentials","path":"/users/current"}
As expected, the server denied the access to the secured resource because the authentication token has been previously revoked.
JWT Example¶
What if you want to have a token that expires after some time (like 24h for example)? We can leverage JWT tokens for that. JWT Tokens are typically formatted as following:
xxxx.yyyy.zzzz
The token is generated and signed by the server. It's possible to decode it easily, but it's not possible to generate a new one unless you know the server secret key
. That very convenient!
How does it work:
- First, we authenticate with
username
andpassword
, - The server responds with a signed JWT Token which contains the user id,
- Susbequent requests are sent with
Authorization: Bearer TOKEN
, - On each request, the server verify the JWT token is properly signed by himself and extracts the user id to identify the user.
token-api¶
The token-api
module contains the TokenService
API:
package com.octoperf.token.api;
import java.util.Map;
/**
* Creates and validates credentials.
*/
public interface TokenService {
String permanent(Map<String, String> attributes);
String expiring(Map<String, String> attributes);
/**
* Checks the validity of the given credentials.
*
* @param token
* @return attributes if verified
*/
Map<String, String> untrusted(String token);
/**
* Checks the validity of the given credentials.
*
* @param token
* @return attributes if verified
*/
Map<String, String> verify(String token);
}
The token service is responsible of generating and validating JWT tokens. Let's see the implementation now.
token-jwt¶
Now let's implement the JWTTokenService
:
package com.octoperf.token.jwt;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.octoperf.date.service.DateService;
import com.octoperf.token.api.TokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Clock;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.compression.GzipCompressionCodec;
import lombok.experimental.FieldDefaults;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Map;
import static io.jsonwebtoken.SignatureAlgorithm.HS256;
import static io.jsonwebtoken.impl.TextCodec.BASE64;
import static java.util.Objects.requireNonNull;
import static lombok.AccessLevel.PRIVATE;
import static org.apache.commons.lang3.StringUtils.substringBeforeLast;
@Service
@FieldDefaults(level = PRIVATE, makeFinal = true)
final class JWTTokenService implements Clock, TokenService {
private static final String DOT = ".";
private static final GzipCompressionCodec COMPRESSION_CODEC = new GzipCompressionCodec();
DateService dates;
String issuer;
int expirationSec;
int clockSkewSec;
String secretKey;
JWTTokenService(final DateService dates,
@Value("${jwt.issuer:octoperf}") final String issuer,
@Value("${jwt.expiration-sec:86400}") final int expirationSec,
@Value("${jwt.clock-skew-sec:300}") final int clockSkewSec,
@Value("${jwt.secret:secret}") final String secret) {
super();
this.dates = requireNonNull(dates);
this.issuer = requireNonNull(issuer);
this.expirationSec = requireNonNull(expirationSec);
this.clockSkewSec = requireNonNull(clockSkewSec);
this.secretKey = BASE64.encode(requireNonNull(secret));
}
@Override
public String permanent(final Map<String, String> attributes) {
return newToken(attributes, 0);
}
@Override
public String expiring(final Map<String, String> attributes) {
return newToken(attributes, expirationSec);
}
private String newToken(final Map<String, String> attributes, final int expiresInSec) {
final DateTime now = dates.now();
final Claims claims = Jwts
.claims()
.setIssuer(issuer)
.setIssuedAt(now.toDate());
if (expiresInSec > 0) {
final DateTime expiresAt = now.plusSeconds(expiresInSec);
claims.setExpiration(expiresAt.toDate());
}
claims.putAll(attributes);
return Jwts
.builder()
.setClaims(claims)
.signWith(HS256, secretKey)
.compressWith(COMPRESSION_CODEC)
.compact();
}
@Override
public Map<String, String> verify(final String token) {
final JwtParser parser = Jwts
.parser()
.requireIssuer(issuer)
.setClock(this)
.setAllowedClockSkewSeconds(clockSkewSec)
.setSigningKey(secretKey);
return parseClaims(() -> parser.parseClaimsJws(token).getBody());
}
@Override
public Map<String, String> untrusted(final String token) {
final JwtParser parser = Jwts
.parser()
.requireIssuer(issuer)
.setClock(this)
.setAllowedClockSkewSeconds(clockSkewSec);
// See: https://github.com/jwtk/jjwt/issues/135
final String withoutSignature = substringBeforeLast(token, DOT) + DOT;
return parseClaims(() -> parser.parseClaimsJwt(withoutSignature).getBody());
}
private static Map<String, String> parseClaims(final Supplier<Claims> toClaims) {
try {
final Claims claims = toClaims.get();
final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
for (final Map.Entry<String, Object> e: claims.entrySet()) {
builder.put(e.getKey(), String.valueOf(e.getValue()));
}
return builder.build();
} catch (final IllegalArgumentException | JwtException e) {
return ImmutableMap.of();
}
}
@Override
public Date now() {
final DateTime now = dates.now();
return now.toDate();
}
}
I'm using jjwt
library for that:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
Nothing fancy here, i'm just using JJWT API as explained on their Github Page. Now, it's time to use this TokenService
in our authentication flow!
user-auth-token¶
The user-auth-token
module is responsible of authenticating a user with the TokenService
. It contains the TokenAuthenticationService
:
package com.octoperf.user.auth.crud;
import com.google.common.collect.ImmutableMap;
import com.octoperf.auth.api.UserAuthenticationService;
import com.octoperf.token.api.TokenService;
import com.octoperf.user.crud.api.UserCrudService;
import com.octoperf.user.entity.User;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.Optional;
import static lombok.AccessLevel.PACKAGE;
import static lombok.AccessLevel.PRIVATE;
@Service
@AllArgsConstructor(access = PACKAGE)
@FieldDefaults(level = PRIVATE, makeFinal = true)
final class TokenAuthenticationService implements UserAuthenticationService {
@NonNull
TokenService tokens;
@NonNull
UserCrudService users;
@Override
public Optional<String> login(final String username, final String password) {
return users
.findByUsername(username)
.filter(user -> Objects.equals(password, user.getPassword()))
.map(user -> tokens.expiring(ImmutableMap.of("username", username)));
}
@Override
public Optional<User> findByToken(final String token) {
return Optional
.of(tokens.verify(token))
.map(map -> map.get("username"))
.flatMap(users::findByUsername);
}
@Override
public void logout(final User user) {
// Nothing to doy
}
}
As you can see, when a user logs in, we return a token which contains the user username
. We'll use that later to find the user again when authenticating him.
The great thing is The entire token logic is encapsulated within the UserAuthenticationService
.
Configuration¶
The example-jwt
module glues everything together:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.octoperf</groupId>
<artifactId>securing-rest-api-spring-security</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.octoperf</groupId>
<artifactId>example-jwt</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>user-entity</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>user-controller</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>token-jwt</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>user-crud-in-memory</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>user-auth-token</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>bootstrap</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.octoperf</groupId>
<artifactId>security-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
Like previously, create an Intellij launcher with the following configuration:
- Main Class:
com.octoperf.Application
, - Working Directory:
$MODULE_DIR$
, - Use classpath of module:
example-jwt
.
Then run it. The application should be running within a few seconds:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.3.RELEASE)
2020-08-28 10:49:45.813 INFO 22282 --- [ main] com.octoperf.Application : Starting Application on t440p with PID 22282 (/home/ubuntu/git/securing-rest-api-spring-security/bootstrap/target/classes started by ubuntu in /home/ubuntu/git/securing-rest-api-spring-security)
2020-08-28 10:49:45.817 INFO 22282 --- [ main] com.octoperf.Application : No active profile set, falling back to default profiles: default
2020-08-28 10:49:48.281 INFO 22282 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http)
2020-08-28 10:49:48.331 INFO 22282 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-08-28 10:49:48.336 INFO 22282 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.37]
2020-08-28 10:49:48.560 INFO 22282 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-08-28 10:49:48.561 INFO 22282 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2568 ms
2020-08-28 10:49:49.121 INFO 22282 --- [ main] o.s.boot.web.servlet.RegistrationBean : Filter tokenAuthenticationFilter was not registered (disabled)
2020-08-28 10:49:49.289 INFO 22282 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: OrRequestMatcher [requestMatchers=[Ant [pattern='/public/**'], Ant [pattern='/error/**']]], []
2020-08-28 10:49:49.333 INFO 22282 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@1a22e0ef, org.springframework.security.web.context.SecurityContextPersistenceFilter@4821aa9f, org.springframework.security.web.header.HeaderWriterFilter@f1d0004, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@32130e61, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@ccf91df, com.octoperf.security.config.TokenAuthenticationFilter@1640190a, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@67514bdd, org.springframework.security.web.session.SessionManagementFilter@48b4a043, org.springframework.security.web.access.ExceptionTranslationFilter@74ea46e2, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@38b8b6c0]
2020-08-28 10:49:49.525 INFO 22282 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-08-28 10:49:49.860 INFO 22282 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
2020-08-28 10:49:49.878 INFO 22282 --- [ main] com.octoperf.Application : Started Application in 5.53 seconds (JVM running for 6.561)
Testing The Application¶
First, let's register on the REST API:
ubuntu@laptop:~$ curl -XPOST -d 'username=john&password=smith' http://localhost:8081/public/users/register
eyJhbGciOiJIUzI1NiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAAKtWyiwuVrJSyk8uyS9ILUpT0lHKTCxRsjI0NTI3Mzc0NDXUUUqtKIAImJuam4IESotTi_ISc1OB-rLyM_KUagHqL4qjRgAAAA.jsmDSIYGoG-EKZr-Yw5G2k3c6Ano69A0nAncA8dnHBw
Here we got a JWT Token now! Then we can also login with this username and password:
ubuntu@laptop:~$ curl -XPOST -d 'username=john&password=smith' http://localhost:8081/public/users/login
eyJhbGciOiJIUzI1NiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAAKtWyiwuVrJSyk8uyS9ILUpT0lHKTCxRsjI0NTI3Mzc0tLDUUUqtKIAImJuam4IESotTi_ISc1OB-rLyM_KUagFKIH8rRgAAAA.E4bC_Vrvm7rD2Ms6KWHwotZUNTbFB7TK_3Wnc1LQpE8
By sending an url-encoded form post request to the endpoint, it returns as expected a random UUID
. Now, let's use the UUID in a subsequent request to retrieve the current user:
ubuntu@laptop:~$ -772:~$ curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInppcCI6IkdaSVAifQ...' http://localhost:8081/users/current
{"id":"john","username":"john","enabled":true}
Nice! We're logged into the system and we could retrieve the current user in Json format. By default, Spring Boot uses Jackson Json API to serialize beans into Json.
Let's now logout from the system:
ubuntu@laptop:~$ curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInppcCI6IkdaSVAifQ...' http://localhost:8081/users/logout
true
If we try to get the current user again with the same authentication token, we should receive an error:
ubuntu@laptop:~$ curl -H 'Authorization: Bearer ...' http://localhost:8081/users/current
{"timestamp":1516184750678,"status":401,"error":"Unauthorized","message":"Authentication Failed: Bad credentials","path":"/users/current"}
As expected, the server denied the access to the secured resource because the authentication token has been previously revoked.
Final Words¶
I hope this ready to use skeleton security layer will enable you to build a secure Rest API using Spring Security. It took a while to figure out how Spring Security works, and how to create this configuration.
As you can see, the system is designed in a way it's easy to replace the authentication logic with another.
We thought it would be a good idea to share this tutorial to help you avoid spending weeks messing around with Spring Security (as we did).