Using Spring Boot for OAuth2 and JWT REST Protection
REST APIs are used in every language and on every platform. Building a secure REST API is a must-have tool in every developer’s arsenal.
In this article, Toptal Freelance Java Developer Sergio Moretti shows how to secure a REST API using Spring Boot.
REST APIs are used in every language and on every platform. Building a secure REST API is a must-have tool in every developer’s arsenal.
In this article, Toptal Freelance Java Developer Sergio Moretti shows how to secure a REST API using Spring Boot.
Sergio has over a dozen years of experience developing enterprise-level applications with Java and RDBMS like Oracle, PostgreSQL, and MySQL.
Expertise
This article is a guide on how to setup a server-side implementation of JSON Web Token (JWT) - OAuth2 authorization framework using Spring Boot and Maven.
An initial grasp on OAuth2 is recommended and can be obtained reading the draft linked above or searching for useful information on the web like this or this.
OAuth2 is an authorization framework superseding it first version OAuth, created back in 2006. It defines the authorization flows between clients and one or more HTTP services in order to gain access to protected resources.
OAuth2 defines the following server-side roles:
- Resource Owner: The service responsible for controlling resources’ access
- Resource Server: The service who actually supplies the resources
- Authorization Server: The service handling authorization process acting as a middleman between client and resource owner
JSON Web Token, or JWT, is a specification for the representation of claims to be transferred between two parties. The claims are encoded as a JSON object used as the payload of an encrypted structure, enabling the claims to be digitally signed or encrypted.
The containing structure can be JSON Web Signature (JWS) or JSON Web Encryption (JWE).
JWT can be chosen as the format for access and refresh tokens used inside the OAuth2 protocol.
OAuth2 and JWT gained a huge popularity over the last years because of the following features:
- Provides a stateless authorization system for stateless REST protocol
- Fits well in a micro-service architecture in which multiple resource servers can share a single authorization server
- Token content easy to manage on client’s side due to JSON format
However, OAuth2 and JWT are not always the best choice in case the following considerations are important for the project:
- A stateless protocol doesn’t permit access revocation on the server side
- Fixed lifetime for token add additional complexity for managing long-running sessions without compromising security (e.g. refresh token)
- A requirement for a secure store for a token on the client side
Expected Protocol Flow
While one of the main features of OAuth2 is the introduction of an authorization layer in order to separate authorization process from resource owners, for the sake of simplicity, the article’s outcome is the build of a single application impersonating all resource owner, authorization server, and resource server roles. Because of this, the communication will flow between two entities only, the server and the client.
This simplification should help to focus on the aim of the article, i.e. the setup of such a system in a Spring Boot’s environment.
The simplified flow is described below:
- Authorization request is sent from client to server (acting as resource owner) using password authorization grant
- Access token is returned to the client (along with refresh token)
- Access token is then sent from client to server (acting as resource server) on each request for protected resource access
- Server responds with required protected resources
Spring Security and Spring Boot
First of all, a brief introduction to the technology stack selected for this project.
The project management tool of choice is Maven, but due to the project’s simplicity, it should not be difficult to switch to other tools like Gradle.
In the article’s continuation, we focus on Spring Security aspects only, but all code excerpts are taken from a fully working server-side application which source code is available in a public repository along with a client consuming its REST resources.
Spring Security is a framework providing an almost declarative security services for Spring-based applications. Its roots are from the first beginning of Spring and it’s organized as a set of modules due to the high number of different security technologies covered.
Let’s take a quick look at Spring Security architecture.
Security is mostly about authentication, i.e. the verification of the identity, and authorization, the grant of access rights to resources.
Spring security supports a huge range of authentication models, either provided by third parties or implemented natively. A list can be found here.
Regarding authorization, three main areas are identified:
- Web requests authorization
- Method level authorization
- Access to domain object instances authorization
Authentication
The basic interface is AuthenticationManager
which is responsible to provide an authentication method. The UserDetailsService
is the interface related to user’s information collection, which could be directly implemented or used internally in case of standard JDBC or LDAP methods.
Authorization
The main interface is AccessDecisionManager
; which implementations for all three areas listed above delegate to a chain of AccessDecisionVoter
. Each instance of the latter interface represents an association between an Authentication
(a user identity, named principal), a resource and a collection of ConfigAttribute
, the set of rules which describes how the resource’s owner allowed the access to the resource itself, maybe through the use of user roles.
The security for a web application is implemented using the basic elements described above in a chain of servlet filters, and the class WebSecurityConfigurerAdapter
is exposed as a declarative way to express resource’s access rules.
Method security is first enabled by the presence of the @EnableGlobalMethodSecurity(securedEnabled = true)
annotation, and then by the use of a set of specialized annotations to apply to each method to be protected such as @Secured
, @PreAuthorize
, and @PostAuthorize
.
Spring Boot adds to all of this a collection of opinionated application configurations and third-party libraries in order to ease the development while maintaining an high quality standard.
JWT OAuth2 with Spring Boot
Let’s now move on the original problem to set up an application implementing OAuth2 and JWT with Spring Boot.
While multiple server-side OAuth2 libraries exist in the Java world (a list can be found here), the spring-based implementation is the natural choice as we expect to find it well integrated into Spring Security architecture and therefore avoid the need to handle much of the low-level details for its use.
All security-related library dependencies are handled by Maven with the help of Spring Boot, which is the only component requiring an explicit version inside maven’s configuration file pom.xml (i.e. library versions are automatically inferred by Maven choosing the most up-to-date version compatible with the inserted Spring Boot version).
Find below the excerpt from maven’s configuration file pom.xml containing the dependencies related to Spring Boot security:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
The app acts both as OAuth2 authorization server/resource owner and as resource server.
The protected resources (as resource server) are published under /api/ path, while authentication path (as resource owner/authorization server) is mapped to /oauth/token, following proposed default.
App’s structure:
-
security
package containing security configuration -
errors
package containing error handling -
users
,glee
packages for REST resources, including model, repository, and controller
Next paragraphs cover the configuration for each one of the three OAuth2 roles mentioned above. The related classes are inside security
package:
-
OAuthConfiguration
, extendingAuthorizationServerConfigurerAdapter
-
ResourceServerConfiguration
, extendingResourceServerConfigurerAdapter
-
ServerSecurityConfig
, extendingWebSecurityConfigurerAdapter
-
UserService
, implementingUserDetailsService
Setup for Resource Owner and Authorization Server
Authorization server behavior is enabled by the presence of @EnableAuthorizationServer
annotation. Its configuration is merged with the one related to the resource owner behavior and both are contained in the class AuthorizationServerConfigurerAdapter
.
The configurations applied here are related to:
- Client access (using
ClientDetailsServiceConfigurer
)- Selection of use an in-memory or JDBC based storage for client details with
inMemory
orjdbc
methods - Client’s basic authentication using
clientId
andclientSecret
(encoded with the chosenPasswordEncoder
bean) attributes - Validity time for access and refresh tokens using
accessTokenValiditySeconds
andrefreshTokenValiditySeconds
attributes - Grant types allowed using
authorizedGrantTypes
attribute - Defines access scopes with
scopes
method - Identify client’s accessible resources
- Selection of use an in-memory or JDBC based storage for client details with
- Authorization server endpoint (using
AuthorizationServerEndpointsConfigurer
)- Define the use of a JWT token with
accessTokenConverter
- Define the use of an
UserDetailsService
andAuthenticationManager
interfaces to perform authentication (as resource owner)
- Define the use of a JWT token with
package net.reliqs.gleeometer.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
@Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
private final PasswordEncoder passwordEncoder;
private final UserDetailsService userService;
@Value("${jwt.clientId:glee-o-meter}")
private String clientId;
@Value("${jwt.client-secret:secret}")
private String clientSecret;
@Value("${jwt.signing-key:123}")
private String jwtSigningKey;
@Value("${jwt.accessTokenValidititySeconds:43200}") // 12 hours
private int accessTokenValiditySeconds;
@Value("${jwt.authorizedGrantTypes:password,authorization_code,refresh_token}")
private String[] authorizedGrantTypes;
@Value("${jwt.refreshTokenValiditySeconds:2592000}") // 30 days
private int refreshTokenValiditySeconds;
public OAuthConfiguration(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder, UserDetailsService userService) {
this.authenticationManager = authenticationManager;
this.passwordEncoder = passwordEncoder;
this.userService = userService;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(clientId)
.secret(passwordEncoder.encode(clientSecret))
.accessTokenValiditySeconds(accessTokenValiditySeconds)
.refreshTokenValiditySeconds(refreshTokenValiditySeconds)
.authorizedGrantTypes(authorizedGrantTypes)
.scopes("read", "write")
.resourceIds("api");
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.accessTokenConverter(accessTokenConverter())
.userDetailsService(userService)
.authenticationManager(authenticationManager);
}
@Bean
JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
return converter;
}
}
The next section describes the configuration to apply to the resource server.
Setup for Resource Server
The resource server behavior is enabled by the use of @EnableResourceServer
annotation and its configuration is contained in the class ResourceServerConfiguration
.
The only needed configuration here is the definition of resource identification in order to match the client’s access defined in the previous class.
package net.reliqs.gleeometer.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("api");
}
}
The last configuration element is about the definition of web application security.
Web Security Setup
Spring web security configuration is contained in the class ServerSecurityConfig
, enabled by the use of @EnableWebSecurity
annotation. The @EnableGlobalMethodSecurity
permits to specify security on the method level. Its attribute proxyTargetClass
is set in order to have this working for RestController
’s methods, because controllers are usually classes, not implementing any interfaces.
It defines the following:
- The authentication provider to use, defining the bean
authenticationProvider
- The password encoder to use, defining the bean
passwordEncoder
- The authentication manager bean
- The security configuration for the published paths using
HttpSecurity
- Use of a custom
AuthenticationEntryPoint
in order to handle error messages outside of standard Spring REST error handlerResponseEntityExceptionHandler
package net.reliqs.gleeometer.security;
import net.reliqs.gleeometer.errors.CustomAccessDeniedHandler;
import net.reliqs.gleeometer.errors.CustomAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class ServerSecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final UserDetailsService userDetailsService;
public ServerSecurityConfig(CustomAuthenticationEntryPoint customAuthenticationEntryPoint, @Qualifier("userService")
UserDetailsService userDetailsService) {
this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
this.userDetailsService = userDetailsService;
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(userDetailsService);
return provider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/signin/**").permitAll()
.antMatchers("/api/glee/**").hasAnyAuthority("ADMIN", "USER")
.antMatchers("/api/users/**").hasAuthority("ADMIN")
.antMatchers("/api/**").authenticated()
.anyRequest().authenticated()
.and().exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint).accessDeniedHandler(new CustomAccessDeniedHandler());
}
}
The code extract below is about the implementation of UserDetailsService
interface in order to provide the resource owner’s authentication.
package net.reliqs.gleeometer.security;
import net.reliqs.gleeometer.users.User;
import net.reliqs.gleeometer.users.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserService implements UserDetailsService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = repository.findByEmail(username).orElseThrow(() -> new RuntimeException("User not found: " + username));
GrantedAuthority authority = new SimpleGrantedAuthority(user.getRole().name());
return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), Arrays.asList(authority));
}
}
The next section is about the description of a REST controller implementation in order to see how security constraints are mapped.
REST Controller
Inside the REST controller we can find two ways to apply access control for each resource method:
- Using an instance of
OAuth2Authentication
passed in by Spring as a parameter - Using
@PreAuthorize
or@PostAuthorize
annotations
package net.reliqs.gleeometer.users;
import lombok.extern.slf4j.Slf4j;
import net.reliqs.gleeometer.errors.EntityNotFoundException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import javax.validation.constraints.Size;
import java.util.HashSet;
@RestController
@RequestMapping("/api/users")
@Slf4j
@Validated
class UserController {
private final UserRepository repository;
private final PasswordEncoder passwordEncoder;
UserController(UserRepository repository, PasswordEncoder passwordEncoder) {
this.repository = repository;
this.passwordEncoder = passwordEncoder;
}
@GetMapping
Page<User> all(@PageableDefault(size = Integer.MAX_VALUE) Pageable pageable, OAuth2Authentication authentication) {
String auth = (String) authentication.getUserAuthentication().getPrincipal();
String role = authentication.getAuthorities().iterator().next().getAuthority();
if (role.equals(User.Role.USER.name())) {
return repository.findAllByEmail(auth, pageable);
}
return repository.findAll(pageable);
}
@GetMapping("/search")
Page<User> search(@RequestParam String email, Pageable pageable, OAuth2Authentication authentication) {
String auth = (String) authentication.getUserAuthentication().getPrincipal();
String role = authentication.getAuthorities().iterator().next().getAuthority();
if (role.equals(User.Role.USER.name())) {
return repository.findAllByEmailContainsAndEmail(email, auth, pageable);
}
return repository.findByEmailContains(email, pageable);
}
@GetMapping("/findByEmail")
@PreAuthorize("!hasAuthority('USER') || (authentication.principal == #email)")
User findByEmail(@RequestParam String email, OAuth2Authentication authentication) {
return repository.findByEmail(email).orElseThrow(() -> new EntityNotFoundException(User.class, "email", email));
}
@GetMapping("/{id}")
@PostAuthorize("!hasAuthority('USER') || (returnObject != null && returnObject.email == authentication.principal)")
User one(@PathVariable Long id) {
return repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString()));
}
@PutMapping("/{id}")
@PreAuthorize("!hasAuthority('USER') || (authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)")
void update(@PathVariable Long id, @Valid @RequestBody User res) {
User u = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString()));
res.setPassword(u.getPassword());
res.setGlee(u.getGlee());
repository.save(res);
}
@PostMapping
@PreAuthorize("!hasAuthority('USER')")
User create(@Valid @RequestBody User res) {
return repository.save(res);
}
@DeleteMapping("/{id}")
@PreAuthorize("!hasAuthority('USER')")
void delete(@PathVariable Long id) {
if (repository.existsById(id)) {
repository.deleteById(id);
} else {
throw new EntityNotFoundException(User.class, "id", id.toString());
}
}
@PutMapping("/{id}/changePassword")
@PreAuthorize("!hasAuthority('USER') || (#oldPassword != null && !#oldPassword.isEmpty() && authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)")
void changePassword(@PathVariable Long id, @RequestParam(required = false) String oldPassword, @Valid @Size(min = 3) @RequestParam String newPassword) {
User user = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString()));
if (oldPassword == null || oldPassword.isEmpty() || passwordEncoder.matches(oldPassword, user.getPassword())) {
user.setPassword(passwordEncoder.encode(newPassword));
repository.save(user);
} else {
throw new ConstraintViolationException("old password doesn't match", new HashSet<>());
}
}
}
Conclusion
Spring Security and Spring Boot permit to quickly set up a complete OAuth2 authorization/authentication server in an almost declarative manner. The setup can be further shortened by configuring OAuth2 client’s properties directly from application.properties/yml
file, as explained in this tutorial.
All source code is available in this GitHub repository: spring-glee-o-meter. An Angular client which consumes the published resources can be found in this GitHub repository: glee-o-meter.
Further Reading on the Toptal Blog:
Understanding the basics
What is OAuth2?
OAuth2 is an authorization framework to enable a third-party application to obtain limited access to an HTTP service through the sharing of an access token. Its specification supersedes and obsoletes OAuth 1.0 protocol.
What is JWT?
JWT stands for JSON Web Token, a specification for the representation of claims to be transferred between two parties. The claims are encoded as a JSON object used as the payload of an encrypted structure which enables the claims to be digitally signed or encrypted.
What is Spring Security?
Spring Security is a framework focused on providing authentication and authorization to Spring-based applications.
What is Spring Boot?
Spring Boot is an opinionated view of the Spring platform and third-party libraries which permits to minimize the configuration of Spring-based application while maintaining production-grade quality level.
Castel Maggiore, Metropolitan City of Bologna, Italy
Member since December 11, 2018
About the author
Sergio has over a dozen years of experience developing enterprise-level applications with Java and RDBMS like Oracle, PostgreSQL, and MySQL.