Securing REST Api with Spring Security
This article will guide you through setting up Spring Security for a REST service created using Swagger specification. It also goes through integration testing of secure REST service. The code behind this article can be found on allAroundJava’s github right here.
Should you need some more practice creating a REST endpoint using Swagger’s Open Api specification, please take a look at this articla.
Maven Dependencies
Lets start securing our REST endpoint by adding Spring Security dependency.
Additionally to create an endpoint interface from Swagger’s Open Api specification and gain access to Swagger annotations, lets add a springfox swagger dependency.
Implementing Secure REST endpoint based on Swagger Open Api
Ok we got the necessary groundwork. Now we can get our hands dirty. Lets first define the Secure REST endpoint with Swagger’s Open Api specification. The endpoint will expose a WebPage resource. This is a very basic resource carrying information about urls of web pages we want to track in our system. Our secure REST service will allow for POSTing and GETting information about a Web Page.
WebPageDto:
type: object
required:
- url
properties:
id:
type: integer
format: int64
url:
type: string
xml:
name: WebPageDto
The /webPages endpoint is defined in the following way. Please note the securityDefinitions specifying HTTP basic authentication and the fact that only the POST method is secured according to the specification.
host: "localhost:8080"
basePath: /
tags:
- name: "Web Pages"
description: "Everything about Web Pages in the system"
schemes:
- http
securityDefinitions:
basicAuth:
type: basic
paths:
/webPages/{id}:
get:
summary: "Retrieve existing web page information along with its price details"
operationId: "getWebPage"
produces:
- application/json
parameters:
- in: "path"
name: "id"
description: "Id of webpage to be retrieved"
required: true
type: integer
format: int64
responses:
'200':
description: "OK"
schema:
$ref: '#/definitions/WebPageDto'
/webPages:
post:
summary: "Add a new web page to web page collection"
operationId: "addWebPage"
produces:
- application/json
security:
- basicAuth: []
parameters:
- in: "body"
name: "WebPageDto"
description: "Web Page Object that should be added"
required: true
schema:
$ref: '#/definitions/WebPageDto'
responses:
201:
description: "Web Page Created"
schema:
$ref: '#/definitions/WebPageDto'
Creating a plain REST service
Ok now we’ve got the specification in place. Using swagger-codegen-maven-plugin we will create our REST service’s interface. If you’re unsure how to do that, please take a look at this article, where I described how the magic is done. Having the interface, we’ll create a simple, plain, unsecured endpoint according to above specification.
@RequiredArgsConstructor
@RestController
class WebPageController implements WebPagesApi {
private final WebPageService webPageService;
@Override
public ResponseEntity addWebPage(@Valid WebPageDto webPageDto) {
WebPage result = webPageService.save(WebPageDtoConverter.fromDto(webPageDto, authenticatedUser));
return ResponseEntity.status(HttpStatus.CREATED).body(WebPageDtoConverter.toDto(result));
}
@Override
public ResponseEntity getWebPage(Long id) {
WebPage webPage = webPageService.findById(id)
.orElseThrow(() -> new NotFoundException("Web Page with this id could not be found"));
return ResponseEntity.ok(WebPageDtoConverter.toDto(webPage));
}
The addWebPage function will be called when we POST with a specific WebPage. For REST controllers, I’m using an additional DTO data layer, just to allow for some more configuration of how the objects are converted to JSON or XML and also not to mix up data objects passed between layers of application. The addwebPage function wil convert the WebPageDto to a WebPage domain object and use a service class to persist it in the database.
By analogy, the getWebPage function is the one which responds to GET calls to our REST endpoint and it will find the WebPage by Id or throw exception if it does not exist.
That is a very basic service. If you feel you need some more practice in creating REST endpoints, take a look here.
REST Endpoint Spring Security Configuration
Ok now that we’ve got our simple endpoint set up, we can go ahead and secure it. Spring Security is implemented using servlet filters and aspects, therefore our REST endpoint is not really concerned with its own security and will not carry any security related code. We will define a security configuration in a Spring Config bean and the framework will take care of the rest.
We use @EnableWebSecurity annotation to enable Spring Secutity in our application. This annotation is useless on its own, however used with a class which implements WebSecurityConfigurer or extends WebSecurityConfigurerAdapter, it gets all its powers. Now WebSecurityConfigurerAdapter has three config methods which allow for a fine grained security configuration.
Function | Responsibility |
---|---|
void configure(HttpSecurity http) | Primary method to configure how particular requests are secured |
void configure(WebSecurity auth) | Primary method to create FilterChainProxy also known as Spring Security Filter Chain |
void configure(AuthenticationManagerBuilder builder) | Allows to define a service which will retrieve user details |
Here’s how the configuration for our endpoint looks.
@EnableWebSecurity
@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/webPages").authenticated()
.anyRequest().permitAll()
.and().csrf().disable();
}
}
The configuration specifies that we are using HTTP Basic authentication (with username and password) to authenticate POST requests to /webPages endpoint. All the other requests remain unauthenticated. We also disable cross site request checks for easier testing.
This is a great first step, but how will our application know its users ?
Defining a user details service
User details service is what tells Spring whether a request to /webPages POST endpoint with username and password is legit or not. In other words it tells Spring Security whether a user is present in our system. The interface has only a single method and you can probably figure out what it does.
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
UserDetailsService implementation should return UserDetails object if the user is authentic or throw an exception when its unknown. There is a way to define a set of in memory user details, but it’s just a very trivial approach. Let us define UserDetailsService on our own. It will simply query the database searching for the right user, identified by username and password.
@RequiredArgsConstructor
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("Username or password invalid"));
return new DhUserPrincipal(user);
}
}
What our UserService implementation does is simply go out to database and search for a User with particular email using a Spring Data JPA repository.
@Component
@Transactional
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public User save(User user) {
return userRepository.save(user);
}
public Optional findByEmail(String email) {
return userRepository.findByEmail(email);
}
}
Now the important part is the UserDetails implementation. UserDetails is an interface which enables us to retrieve the basic information about our user, includinig its username and password. For that we have created the DhUserPrincipal, an implementation of UserDetails, which takes a User entity as a parameter.
Since we’re not using a role based access in this application, all ouf our authenticated users have the same USER role.
@RequiredArgsConstructor
class DhUserPrincipal implements UserDetails {
private final User user;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority("USER"));
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.isEnabled();
}
public User getUser() {
return user;
}
}
The last bit is to inform our SecurityConfig class to use the UserDetailsService we have created.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
Since its never a good idea to store open text passwords, passwords stored in Users database are encoded with BCrpytPasswordEncoder.
Integration Testing Secure REST Service
Integration testing of secure REST service is very similar to testing the regular, unsecured service. All we need to remember is to have a user with encrypted password available to our UserService to successfully authenticate requests.
The below Spock test in Groovy shows an example integration test for both POST and GET methods. For a broader reference on configuring integration tests of REST services, take a look here.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = [TestJpaConfig, AppConfig, SecurityConfig])
@EnableAutoConfiguration
class WebPageControllerIntegrationTest extends Specification {
@Autowired
private TestRestTemplate restTemplate
@Autowired
private UserService userService
@Autowired
private WebPageService webPageService
@Unroll
def "Posting for a new Web Page with #TEST_TYPE"() {
def url = "http://someurl.com"
def webPageDtoHttpEntity = createWebPageDtoHttpEntity(url)
User user = new User(enabled: true, email: USER_EMAIL, password: BCrypt.hashpw("password", BCrypt.gensalt()))
userService.save(user)
when: "Posting new Web Page"
def responseEntity = restTemplate.withBasicAuth(USER_EMAIL, PASSWORD)
.postForEntity("/webPages", webPageDtoHttpEntity, WebPageDto, [:])
then: "Status is unauthorized"
responseEntity.statusCode == HTTP_STATUS
where:
TEST_TYPE | USER_EMAIL | PASSWORD | HTTP_STATUS
"correct password" | "adam@allaroundjava.com" | "password" | HttpStatus.CREATED
"bad password" | "someone@allaroundjava.com" | "this is bad" | HttpStatus.UNAUTHORIZED
}
private static HttpEntity createWebPageDtoHttpEntity(String url) {
def webPage = new WebPageDto(url: url)
return new HttpEntity(webPage, new HttpHeaders(contentType: MediaType.APPLICATION_JSON))
}
def "Getting a Web Page"() {
User user = new User(enabled: true, email: "adam2@allaroundjava.com", password: "password")
WebPage webPage = new WebPage(userOwner: user, url: "some.com")
userService.save(user)
webPageService.save(webPage)
when: "Posting new Web Page"
def responseEntity = restTemplate
.getForEntity("/webPages/${webPage.id}", WebPageDto, [:])
then: "Status is unauthorized"
responseEntity.statusCode == HttpStatus.OK
}
}
The test for POST method simply makes two requests, one with a correct credentials and one with incorrect ones and asserts that correct HTTP status is in a response from our web service where we implemented REST Security.
Here’s some more articles on REST.
- An article onREST approach fundamentals
- Here’s a guide on creating REST services with Swagger’s Open API with design first appoach
- A deep dive into Unit Testing Rest Controllers
- Integration tests in Mecical Center application on allAroundJava Github