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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
<?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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
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
or httpBasic
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
|
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
:
Simple Example
user-auth-uuid
This module contains a UserAuthenticationService
which is based on a simple random UUID.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
<?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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: 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:
1
2
3
4
|
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:
1
2
3
4
|
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:
1
2
3
4
|
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:
1
2
3
4
|
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:
1
2
3
4
|
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:
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
and password
,
- 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
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:
1
2
3
4
5
|
<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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
<?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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: 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:
1
2
3
|
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:
1
2
3
4
|
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:
1
2
3
|
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:
1
2
3
4
|
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:
1
2
3
4
|
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).
Please provide the link for downloading source code. Is there a difference in securing Rest Apis and JSPs?
In reply to Abhishek
Hi Abhisek, You can download the complete source code on Github project. There is no real difference in securing Rest Api endpoints and JSPs. Main difference will be that Rest APIs are mostly
@RestController
(assuming responses have a body like a json doc) and JSPs are usually@Controller
annotated.Also Check @Pre and @Post annotations on Spring’s website to fine-tune security per controller’s method.
This was very helpful for me in clearing doubts. I spent two weeks to understand the flow of spring security to create a login system using spring boot at backend and angular at frontend. This blog helped me a lot and solved my problem.
In reply to Shashank Rajput
Glad you found this article helpful! Cheers.
I was using your tutorial to secure my app. But now i have some problems. When i try to access to public endpoints my app returns 404 Not Found. Any idea where I made mistake ?
In reply to Thomas
Hi Thomas,
Please make sure your application modules containing your controllers are correctly declared as dependencies.
HTTP 404 Not Found
typically occur in that case. Otherwise try to ask for help on communities like StackOverFlow.In reply to Thomas
One reason it did not work for me in the beginning was that in my PublicUsersController my UserAuthenticationService and UserCrudService got accidentally initialized to null. I had @NonNull UserCrudService users = null; Once I corrected that everything worked
Very nice articlet, thanks for the reply I have just implemented the security working via this article
In reply to vikas
Hi Vikas, I’m glad you liked our article and could implement it in your own web application! Best regards.
I was trying to use your code in Spring-Web application and not in Spring-Boot. And it did not work. Can you suggest changes to make this example work in Spring-Web?
In reply to Rajib
Hi Rajib,
Unfortunately without more information about your issue, It’s difficult to find out what’s wrong within your application. Spring Security Documentation might help you.
In reply to Rajib
@Rajib are u using Eclipse rather than IntelliJ. If OK, under your launch config, under classpath tab, add all the projects (except UIID module) and everything would be nice.
Hi Jerome,
Can you please do a tutorial on how to create token with specific life span like token is only valid for 24 hours etc.
In reply to Abdul Ahad
Hi Abdul,
That’s a good idea! Especially because we already have implemented such kind of logic within our server using Json Web Tokens. Come back in a few days, and you should see this tutorial online here :)
Cheers
Hi Jerome, I got it to work by adding an an empty class:
java public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer { }
But I have another question - In this example, we are storing the users in a Map in classSimpleAuthenticationService
. I am also planning to do the same, but how will I make it work in a clustered / distributed environment? By any chance, can you modifySimpleAuthenticationService
to support clustering? I mean, if I deploy this application across clusters (for example multiple WebSphere nodes), how will we share the users map across clusters? Any reference implementation will be of great help.In reply to Rajib
Hi Rajib,
Thanks for the insight. Most web application store users in a database (like MySQL, Elasticsearch etc.). Therefore, in a clustered / distributed environment, the database is typically accessible from all the web servers. Also, when using JWT tokens with the same secret key the authentication should work on any server. It’s the perfect stateless authentication!
If still want to store users in a
Map<String, User>
, you can use Hazelcast. It’s an in-memory data grid which has distributedMap
implementations. It’s pretty easy to use! We use it for a distributed event-bus in OctoPerf Rest Server.Hi Jerome, I am trying to use Hazelcast now, but having a hard time integrating the same with Spring Web. Please note, I am trying to integrate Hazelcast with Spring Web (not Spring Boot) and I am looking for java annotation based approach. But I am not able to get any convincing article on this. Any help in this respect will be greatly appreciated.
In reply to Rajib
Hi Rajib,
I guess you are trying to setup Spring sessions with Hazelcast, which is quite difficult. Is it what you are trying to do? It really depends on what you want to do.
Hazelcast in itself is very simple to configure and create via annotations based spring configuration (using
@Configuration
and@Bean
). Here is an extract of our code which uses Hazelcast clustering too:But we use Spring Boot, which automatically creates an instance of
HazelcastInstance
when aConfig
bean is defined. Otherwise, you may want to create it yourself by adding to theHazelcastConfig
Not sure if this helps, but it’s difficult to have an answer to your question as you’re not clear enough about your needs. Have you tried asking on StackOverflow?
A very welcome tutorial, thank you. The problem I have is that I cannot get the NoRedirectStrategy to work. My Spring Boot 2.0.2 keeps returning a 302 instead of a 4xx whenever I try to access a protected URL without authentication. Perhaps it is to do with the NoRedirectStrategy being added to the successhandler, and not to a failurehandler? StackOverflow is full of Spring Boot 302 questions but none of these seem to solve the issue. Any pointers greatly appreciated.
In reply to Charles
Hi, Make sure you don’t have duplicate Spring Security configuration in your classpath. Check out the sample code i’ve shared on Github, it doesn’t perform any HTTP 302 Moved Temporarily.
Best Regards,
In reply to Charles
Thank you for your response. I finally solved my issue when I realised that I was trying to set up the project using https, not http. I had added a secure channel requirement to the security config. This meant that the server kept redirecting to https, hence the 302. Removing the secure channel in config has resolved the issue for now and I will return to it when I’m ready to move to production. Thanks again for the feedback.
I was wondering how you would extend this example to include multiple access levels?
In reply to Ferrin Katz
Hi Ferrin,
You can very easily manage user access levels by defining multiple roles. Then, you can have fine grain access control in your code by using @PreAuthorize and @PostAuthorize annotations.
If you are going to use JWT you don’t need to implement any distributed cache for the user session, just store the user details that you need for authentication and authorization in the token, and keep your application stateless. Here the author kind of miss on the point storing only the username and then getting the user with username from the users service, luckily its and in memory store.. but you wouldnt want that for a production environment. Also storing the username having to iterate over the full collection of users to get the correct user its not very efficient, at least should’ve stored the id to get the user back in around O(1) time from the map instead of O(N) iterating.
This implementation of JWT is also incomplete, you will need to implement some revocation mechanism, like token blacklisting. You will need to google a bit.
In reply to Alejandro Bachi
Hi Alejandro,
The code presented here is to get you started. It’s obviously not intended to be used as is in a real application. You’re just pointing flaws made deliberately. The purpose here is not to write an efficient in-memory user store, nore designing a hacker proof JWT token system. The goal is to provide a simple starting point for designing a user login and registration system using Spring Boot and Spring Security.
Regards,
Hi Jerome,
Great tutorial, this helped me a lot!
Regards,
I tried following the tutorial but now I’m somewhat stuck at point 5.1 as the User class shown here doesn’t provide a builder() function. What should it look like? Any help would be appreciated!
In reply to Lea
Hi Lea, Make sure Lombok is properly configured in your IDE. The
builder()
method is generated by this library.In reply to Lea
I downloaded the project off spring but im getting the error in .builder() in the UUIDAuthenticationService.
Very good job!
Logout issue using token based authentication. Is it correct that method logout() does nothing? I’m asking because I can login but after logout I still can execute some secured methods? I don’t know how I can remove old tokens?
In reply to t2
Hi t2,
The
logout()
method does nothing in this example. But, you could store randomly generated tokens in a localMap
and remove them on logout. That way, the token no longer works once logged out.Hi, Thank you for the article. I have to access a rest service with a username and a password and acquire a token, and then use this token to call other functions. How can I do this with Spring Boot and restTemplate please? How can we manage the calls with interceptor, and refresh the token if expired? Many thanks
In reply to Roy Silva
Hi Roy,
I suggest you to read the Consuming Rest Web Service tutorial on Spring official website. It explains very well how to use the
RestTemplate
.Regards,
great article , the best practices of securing rest api use spring security ,thank you,i have been google several days, this article is best .
Hello, I was following your Simple Example (not JWT), did everything the same as you did - the same SecurityConfig etc., but I’m getting the 401 Unathorized when trying to access /public/users/register endpoint. Or any other endpoint for that matter. Also, I tried to modify the SecurityConfig configure(HttpSecurity http) method like this (and removed all other configure methods):
But this still throws 401 Unauthorized on any given request. I have no idea why. Please help
In reply to Greg
Hi Greg,
There is necessarly something you are missing. Start again from the sample project and use exactly the same
SecurityConfig
. Make sure to configureWebSecurity
properly:Spring Security is very sensitive to even a slight settings modification.
In the PublicUsersController, you use request parameters for user id and password. That may not be a safe practice to my understanding. It is better to have the data as a request body.
In reply to Ver
Hi Ver,
What makes you think it’s safer to put
username
andpassword
parameters into the request body? First, it’s not safer. You should use HTTPS to secure the access to your website.Second, it’s already within the request body as it’s an Form-UrlEncoded Http Post.
You mentioned in the comments that you should always use HTTPS to secure access to the API. Is there anyway to do this by amending SecurityConfig? I have tried adding
requiresChannel().anyRequest().requiresSecure();
but that seriously messes up the API, and pretty much everything stops working. Any pointers would be greatly appreciated.
In reply to Charles
Hi Charles, It’s usually done using SSL Termination. We use HAProxy in front of our Apache Tomcat server to do that. It’s much easier to setup than within Java / Apache Tomcat.
The best article. Thank you!
Thanks for the tutorial !! What if I want to exact real database instead of in memory. Could you please provide that logic.
In reply to rohit
Hi Rohit,
Simply implement your logic by providing your own implementation of
UserCrudService
. Unfortunately, as the logic depends on the database you plan to use, I can’t much help at this point as the code greatly varies for that reason.Thanks for the great write up. I’ve a question. I’m trying to implement this without spring boot. And I want to know is the generated UUID token is persisted. if yes, how? Auth token is per session and so i dont want to save in the database.
In reply to RD
Hi RD, Just implement the
UserAuthenticationService
with your own logic (like keeping tokens in memory for example). None of the examples above persists the token.Hi!
Greate material. If you don’t mind I will use some source code for an open source scaffold for java + angular 6. References will link to this post.
I was getting always a 403 in any rest request, even in /public/**. I found that in SecurityConfig.configure(final HttpSecurity http) you had: addFilterBefore(restAuthenticationFilter(), AnonymousAuthenticationFilter.class).authorizeRequests() .anyRequest().authenticated()
Thus, any request was mapped to this filter and getting 403 everytime. Changin anyRequest() to .requestMatchers(PROTECTED_URLS).authenticated() the public routes are properly accesible without a token and the rest of URLS are secured.
In reply to Guillermo
Hi, Thanks this has been fixed in the code!
Thanks for the article. I have a question though. What is the difference between disabling session managements by
http.sessionManagement().disable()
and setting a stateless creation policy byhttp.sessionManagement().sessionCreationPolicy(STATELESS)
?In reply to Artem
Hi Artem, In the first case, you don’t have any session. In the second case, you have a stateless session: A stateless session bean does not maintain a conversational state with the client.
Thanks for the tutorial, i have a little problem, when i try to do any request to the springboot api all the request are filtered, tha app try to get the token from the public request and private alike i don’t know what is happening, do you have any idea?
In reply to juan
Hi, the issue has been fixed within the code. Thanks for reporting! Please
git pull
the changes.In reply to juan
For anyone having issues with all of the pages being protected, the issue is that /error/ is not included in the PUBLIC_URLs mapping and if you improperly post data to /register or /login it will actually show you a 403 instead of the error page. You were actually able to access the public page, but when the app redirects to the error response, the 403 is shown here.
Changing PUBLIC_URLS to include /error/** solved my issue
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher( new AntPathRequestMatcher("/public/"), new AntPathRequestMatcher("/error/") );
This is a great article, thanks! Maybe the most useful out there in this subject. Even though, it would be cool to have a similar article which explains the same auth flow under WebFlux. If somebody found a good article on webflux security feel free to share!:)
Thanks for a topic! it’s realy helpful! But I can’t run this example with “java -jar …” command. How I may run it in this way? Coluld you help me with this please?
Thanks!
Amazing Post! I loved the way you explain and simplify what must to be simplified.
In reply to Garry
Hi Garry, Glad you like it! I guess I’m too lazy to make things overly complicated.
Hi, I’m creating dynamic web app using spring mvc , hibernate . How can i create a secure API with spring security . How to configure method accessing using tokens
Hi, It’s the purpose of this article.
Really great post! I’m new in spring and i have some understanding problem using this post.
In reply to Alex
Hi Alex, To answer your questions:
@Override
, you cannot add the beans to the method signature for injection. So you need to call them directly within the config. This annotation is needed to provide the beans to Spring Security (which uses them outside this config).I suggest you to take time reading Spring and Spring Security documentation to get a better understanding of how it works (although I must admit both have overly complicated internals).
Hello ! Great tutorial, definitely the best online reference for authentification with a Spring REST Controller ! I found out that you do not have initialized your field UserAuthenticationService auth in your Token Authentication Provider with a bean, I thought I would let you know !
Thank you again !
it was very helpful. thanks!
I have a question though. you set TokenAuthenticationProvider on SecurityConfig 2 times. first,
I’m wondering if the provider works for two different things. i tried without first “auth.authenticationProvider(provider);” -> not working, without second " .authenticationProvider(provider)" -> working
In reply to Jimmy
Hi Jimmy, From what I remember, the authentication provider must be injected in two different locations so Spring Security works consistently. You can try other combinations but i’m pretty sure it won’t work well if not injected in both locations.
Hi Jerome! Very useful tutorial. Is there a way to use OpenID Connect, let’say Google, with this approach? Thank you!
In reply to Robert
Hi, I think you should use Spring social in this case.
Hi Jerome,
I downloaded the code from the GitHub and tried using the same in postman but I am getting 401 for each and every request I try to fire. Any help is appreciated. I tried attaching the screenshot but seems it’s not permitted.
PFB request and response. http://localhost:8082/public/users/register Content-Type:application/x-www-form-urlencoded username:saumik password:saumik
Response : 401 unauthorized.
Thanks, Saumik
In reply to Saumik Chaurasia
Hi, make sure you have lombok installed in your IDE.
Good day! Thanks for the article, I finally found a normal working instruction for setting up Spring Security! Everything is working!
Hi Jerome! Very useful tutorial. how to deploy with tomcat?
In reply to webb
Hi webb, i’m not sure I understand your question. Spring Boot has already Tomcat built-in. Otherwise, build a WAR using Maven and deploy your war file on your Tomcat server.
Super nice article. Thank you for that! Ask the Spring people if you could set it onto the spring.io pages!
Get some positive hints:
In reply to Dirk Schumacher
Hi Dirk,
Glad you appreciate the article! Let me answer to your questions:
Remember the code provided here is not suitable for production unless hardened / reworked to fit the security level of your company.
Hi Jerome,
Thanks for publishing this example - showing how to authenticate in a REST based API using Spring (Security). The article really provides a simplistic and yet a flexible way to authenticate a User with security filters / providers.
Quick questions:
Thanks. P
In reply to Pankaj
Hi Pankaj, to answer your questions:
@AuthenticationPrincipal
is filled in by Spring Security using AuthenticationPrincipalArgumentResolver. It’s done automatically,@AuthenticationPrincipal
annotation.In this simple example, the token is used to find the user. But, you could also imagine having a token which is a session id, then retrieve the user id from within the session (by storing the user id when creating the session).
How does your logout work if you don’t invalidate the token? @Override public void logout(final User user) { // Nothing to doy }
also how does it get removed on the InMemoryUser version, if you don’t remove it from the HashMap?
In reply to Apostol Aurelian
Hi Apostol, The code provided is just an example. You could store authentication tokens in a database. On logout, you would then remove them from the database. See the code provided more as a template, ready to be filled with your own logic.
I get the error mentioning that the TokenAuthenticationProvider could not be created TokenAuthenticationProvider required a bean of type ‘.UserAuthenticationService’ that could not be found.
Also, when creating TokenAuthenticationService, I get the error “Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘JWTTokenService’ “.
Any Idea what is the reason?
In reply to soorya
Please install Lombok in your IDE.
In reply to soorya
You’re using Java 9 or above in which JAXB is deprecated. You can either downgrade to Java 8 or follow the instructions (https://www.jesperdj.com/2018/09/30/jaxb-on-java-9-10-11-and-beyond/)
Hope it helps.
Thanks a lot for teaching me the basic spring security architecture.
That’s a great tutorial, but you have one bug here. Tokens will never expire :)
You set expire time in claims, it should be set in Jwts builder directly:
In reply to Richard
Hi Richard,
In fact, it’s pretty much the same since calling
Jwts.setExpiration(...)
invokes theclaim.setExpiration(...)
method if you check theJwtBuilder
class source-code:Both are valid and should yield the same result.
Hi. Great article and guide. Very usefull and production grade ready. Awesome :).
I have a one question about the adding filter part:
What I found is that option
setEnabled(true)
makes this filter disabled and make this one out of order . Is that intentend operation or a bug?In reply to Szymon Dudziak
Hi Szymon, This portion of code is needed otherwise the filter gets registered in the wrong spring security filter chain.
Hi, This is an excellent article. Thanks for publishing this. I have a question though . In the SecurityConfig Class , you have referenced method “authenticationmanager()” .. but I dont see its implementation anywhere. Is it injected by lombok , if so I dont see any lombok annotations on the SecurityConfig class . The line I am talking about is “filter.setAuthenticationManager(authenticationManager());”
Thanks!!!
In reply to Karthik K
Hi Karthik, It’s inherited from parent abstract class
WebSecurityConfigurerAdapter
.Hi Jerome, it’s awesome blog.I was looking for such blog I will work on this.
TokenAuthenticationProvider retrieveUser() method gives null pointer exception when calling UserAuthenticationService.findByToken() method. I have tried autowiring the class but get cylic bean declaration issue.
Can you help me here
In reply to Mayur Maed
Hello,
Thank you very much for this tutorial! Could you elaborate on the untrusted method of the JwtTokenService? It’s unused, what is it meant for?
Thanks
Hello Jerome! Thank you for helpful acticle, its has helped me alot with my Spring Boot project.
I have tried to get roles working with annotations in controller methods
When requesting uri having above annotation it will return 403 with json payload having long trace.
The following snippet is from log.
Do you have suggestions what I should start investigating next?
In reply to jaluke
Did you get this working? I also have the same problem with the user role
@Jerome Why do we need logout when the session is stateless and we pass an auth token on every request ?
Did you get this working? I also have the same error, @Jerome! Awesome tutorial btw. I have enhanced your code with full authentication to database, but when adding user role, it keeps failing with Forbidden 403.
I can send my project to you if you wish. Thanks!
Great article. A note - the method JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey); in jjwt has been deprecated and will be removed at some point. https://stackoverflow.com/questions/40252903/static-secret-as-byte-key-or-string/40274325#40274325
The secret shouldn’t be a base64 encoded string anymore.
I want to use custom permissions with method security. Accordingly, my User model and UserDetails implementation does not return an empty Array in the getAuthorities method. When trying to use method-security I get
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext.
I suppose there should be some way to
Just want to say many thanks to you for your MEGAUSEFULL article. Haven’t yet read it completely, but your idea with redirection disabling is awesome. I tried to solve this weird 302 for my web-app for a whole day. This approach could be very useful in case of microservices with one auth service. Thank you, Jérôme!
Great demo. Was able to make it work with you example. However, when I have tried to implement in my application i get a 404 not found using the /users/current endpoint. I tried debugging the code and found that TokenAuthenticationFilter.successfulAuthentication is called and the authResult is authenticated, so it seems like the JWT is accepted but i keep getting the 404. Any idea on what the issue might be? Thanks again.