7 Developing RESTful Web services with Spring Boot

This chapter covers

  • Designing and building RESTful Web services with Spring Boot
  • Exception handling in RESTful Web services
  • Developing unit test cases to test RESTful Web services
  • Documenting the RESTful Web services through OpenAPI
  • Implementing different versioning strategies for RESTful Web services
  • Techniques for securing RESTful Web services

In the microservice-based architecture, it is a common practice to expose application functionality in terms of RESTful APIs. These APIs can then be accessed via a range of application devices, such as desktop applications, mobile devices, as well as other APIs.

In this chapter, we’ll introduce you to designing and building RESTful APIs with Spring Boot. You’ll also learn to document the API, so the API consumers can find required details about the API, such as the request, response structures, HTTP return codes, etc. Finally, you’ll learn to develop unit test cases to test the API. Lastly, we’ll show you how to secure your RESTful API. Let’s get started.

7.1 Developing a RESTful API with Spring Boot

A RESTful API (also known as REST API) is an application programming interface that follows the constraints of REST architectural style. REST is an acronym for representational state transfer and was created by Roy Fielding (http://mng.bz/Exyq). In a REST API, when a client requests a resource from the server, the server provides a representation of the state of the requested resource to the client. This representation can be delivered through various formats, such as JSON, plain text, HTML, and others. However, JSON is the most widely used format in the REST API parlance.

Spring Boot provides built-in support in the framework to design and build REST APIs. Spring Boot is one of the most popular frameworks in the Java space for developing REST APIs. In this section, we’ll explore developing a RESTful API with Spring Boot.

7.1.1 Technique: Developing a RESTful API using Spring Boot

In this technique, we’ll demonstrate how to develop a RESTful API using Spring Boot.

Problem

Previously, you’ve used the Course Tracker Spring Boot application with Thymeleaf as the frontend. You now need to expose the Course Tracker application as a RESTful API. Exposing application backend functionality as RESTful API allows the decoupling of application backend with the frontend UI. This design approach lets you opt for the application frontend frameworks (e.g., Angular, React, Vue, etc.) of your choice without being tightly coupled with the backend.

Solution

Designing RESTful APIs with Spring Boot is relatively easy, as the framework provides built-in support for it. These days Spring Boot is the de facto choice for Java developers to build RESTful APIs. If you are following the previous chapters, then you are already aware of most of the content for building a RESTful API with Spring Boot.

In chapter 3, we discussed the use of Spring Data and talked about the approaches to configuring and using a database in a Spring Boot application. In chapter 5, we demonstrated building Spring Boot applications by using Spring controllers in conjunction with Spring Data repositories.

Source code

The final version of the Spring Boot project is available at http://mng.bz/NxzE.

With this technique, you’ll build a RESTful API for the Course Tracker application. It will expose the REST endpoints shown in table 7.1.

Table 7.1 REST endpoints exposed by the Course Tracker API

Endpoint

Operation type

Purpose

/courses/

GET

Returns all available courses from the application

/courses/{id}

GET

Returns a course with the supplied course ID

/courses/category/{name}

GET

Returns the list of courses with the supplied course category name

/courses/

POST

Creates a new course

/courses/{id}

PUT

Updates the course for the supplied course ID

/courses/{id}

DELETE

Deletes a course with the supplied course ID

/courses/

DELETE

Deletes all courses from the application

Table 7.1 contains the REST endpoints that let you perform the CRUD operations in the Course Tracker application. To keep the example simple, we’ve only introduced a limited number of endpoints. In a production application, you may define more REST endpoints. For instance, you can have a few more GET endpoints that let you filter application data to meet application requirements. However, to demonstrate the concepts, we’ll use these REST endpoints throughout this chapter, as this endpoint covers the fundamental operations (CRUD) that most APIs support.

In the Course Tracker application, we are managing Course details. Therefore, we will define the course business entity. The following listing shows this class.

Listing 7.1 The course entity

package com.manning.sbip.ch07.model;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
 
import lombok.Data;
 
@Data
@Entity
@Table(name = "COURSES")
public class Course {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    private Long id;
 
    @Column(name = "NAME")
    private String name;
    @Column(name = "CATEGORY")
    private String category;
 
    @Column(name = "RATING")
    private int rating;
 
    @Column(name = "DESCRIPTION")
    private String description;
}

The Course is a Java POJO that models the course details in the application with fields such as course id, name, category, rating, and description. Next, let’s define the CourseRepository interface, which lets us manage the courses in the database.

Listing 7.2 The CourseRepository interface

package com.manning.sbip.ch07.repository;
 
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
 
import com.manning.sbip.ch07.model.Course;
 
@Repository
public interface CourseRepository extends CrudRepository<Course, Long> {

    Iterable<Course> findAllByCategory(String category);
}

The CourseRepository interface extends the CrudRepository interface and defines a custom method findAllByCategory(..) that finds all courses belonging to a specific category.

Let’s now define the service layer with an interface that provides supported operations in the application. The following listing shows the CourseService interface.

Listing 7.3 The CourseService interface

package com.manning.sbip.ch07.service;
 
//imports
 
public interface CourseService {
 
    Course createCourse(Course course);

    Optional<Course> getCourseById(long courseId);

    Iterable<Course> getCoursesByCategory(String category);

    Iterable<Course> getCourses();

    void updateCourse(long courseId, Course course);

    void deleteCourseById(long courseId);
 
    void deleteCourses();
}

The methods defined in listing 7.3 are self-explanatory and contain method declarations that allow us to perform the CRUD operations in the application. Let’s now provide a default implementation that provides implementations of these methods.

Generally, it is a best practice to define an interface consisting of the operations supported in the API. This interface provides a contract to the controller with the operations supported in the service layer. You can then provide a concrete class that implements these operations. Further, in the controller class, you use the interface name instead of specifying the actual implementation class. This allows you to decouple the controller from the actual implementation. In the future, if you need to provide a different implementation of the service layer, your controller class is not impacted, as it uses the interface and is not tied to a specific implementation. Listing 7.4 shows the CourseServiceImpl class.

Listing 7.4 The CourseServiceImpl class

package com.manning.sbip.ch07.service;
 
//imports
 
@Service                                                   
public class CourseServiceImpl implements CourseService {

    @Autowired                                             
    private CourseRepository courseRepository;


    @Override
    public Course createCourse(Course course) {
        return courseRepository.save(course);
    }
 
    @Override
    public Optional<Course> getCourseById(long courseId) {
        return courseRepository.findById(courseId);
    }
 
    @Override
    public Iterable<Course> getCoursesByCategory(String category) {
        return courseRepository.findAllByCategory(category);
    }
 
    @Override
    public Iterable<Course> getCourses() {
        return courseRepository.findAll();
    }
    @Override
    public void updateCourse(Long courseId, Course course) {
 
        courseRepository.findById(courseId).ifPresent(dbCourse -> {
            dbCourse.setName(course.getName());
            dbCourse.setCategory(course.getCategory());
            dbCourse.setDescription(course.getDescription());
            dbCourse.setRating(course.getRating());
       
            courseRepository.save(dbCourse);
        });
    }
 
    @Override
    public void deleteCourses() {
        courseRepository.deleteAll();
    }
 
    @Override
    public void deleteCourseById(long courseId) {
        courseRepository.deleteById(courseId);
    }
 
}

Annotated with @Service to indicate it’s a service

Autowires the CourseRepository to perform the database operations

The CourseServiceImpl class is annotated with @Service annotation to indicate it’s a service. Recall that @Service is a Spring stereotype annotation that indicates the annotated class is a service class and contains business logic. Further, it uses the CourseRepository to perform the necessary database operations.

We are now left with defining the CourseController that defines the REST endpoints. A Spring controller contains one of more endpoints and accepts the client requests. It then, optionally, uses the services offered by the service layer and generates a response. It wraps the response in a model and shares it with the view layer. A RestContoller also performs a similar activity. However, instead of wrapping the response in the model and sharing to the view layer, it binds the response to the HTTP response body, which is directly shared with the endpoint requester. The following listing shows the CourseController class.

Listing 7.5 The CourseController class

package com.manning.sbip.ch07.controller;
 
import java.util.Optional;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
 
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import com.manning.sbip.ch07.model.Course;
import com.manning.sbip.ch07.service.CourseService;
 
@RestController                                                   
@RequestMapping("/courses/")
public class CourseController {
 
    @Autowired
    private CourseService courseService;

 
    @GetMapping                                                   
    public Iterable<Course> getAllCourses() {
        return courseService.getCourses();
    }

    @GetMapping("{id}")                                           
    public Optional<Course> getCourseById(@PathVariable("id") long
 courseId) {
        return courseService.getCourseById(courseId);
    }
 
    @GetMapping("category/{name}")                                
    public Iterable<Course> getCourseByCategory(@PathVariable("name")
 String category) {
        return courseService.getCoursesByCategory(category);
    }

    @PostMapping                                                  
    public Course createCourse(@RequestBody Course course) {
        return courseService.createCourse(course);
    }

    @PutMapping("{id}")                                           
    public void updateCourse(@PathVariable("id") long courseId,
 @RequestBody Course course) {
        courseService.updateCourse(courseId, course);
    }

    @DeleteMapping("{id}")                                        
    void deleteCourseById(@PathVariable("id") long courseId) {
        courseService.deleteCourseById(courseId);
    }

    @DeleteMapping                                                
    void deleteCourses() {
        courseService.deleteCourses();
    }
 
}

The RequestMapping annotation specified the route or the path to the API. In this example, we have defined the path /courses/ so that all HTTP requests to the /courses/ path are redirected to this controller.

A GetMapping is a special type of RequestMapping that handles only the HTTP GET request. As no path is specified in this endpoint, it is the default endpoint for the HTTP GET /courses/ endpoint.

Handles HTTP GET requests for the path /courses/{id}. The {id} is a path variable and replaced with an appropriate value, e.g. /courses/1, where 1 is the value of the path variable ID.

Handles HTTP GET requests for the path /courses/category/{name}. The {name} is a path variable and replaced with an appropriate value (e.g., /courses/ category/Spring, where Spring is the value of the path variable name).

Handles HTTP POST requests for the path /courses/. An HTTP POST request accepts a request payload. You use the @RequestBody annotation to specify the request body. Note that the requester typically sends a JSON payload, and in the endpoint you expect a Java POJO class that represents the JSON payload. Spring Boot internally performs this deserialization to convert the JSON to the Java type.

Handles the HTTP PUT operations for the path /courses/{id}. The HTTP PUT operation is used to perform the update operations. In this endpoint, we expect the ID of the resource that needs to be updated and the updated representation of the resource in the HTTP request payload. We use the @RequestBody to accept the request payload.

Represents the HTTP DELETE operation for the /courses/{id} path. In this endpoint, we delete the course for the supplied course ID.

Represents the HTTP DELETE operation for the /courses/ path. In this endpoint, we delete all available courses.

Listing 7.5 defines all the endpoints listed in table 7.1. We’ll explore this class in greater detail in the discussion section of this technique. However, one thing you should take note of is the use of @RestController annotation instead of the previously used @Controller annotation.

Testing REST endpoints

Several utilities can be used to test REST endpoints. You can use Postman (https://www.postman.com/) tool, which provides a GUI to test the endpoint. One nice feature of Postman is that you can group related endpoints to create a collection. You can export the collection and share it with others, who can import it in their Postman and test the same endpoints.

If you prefer command-line tools, you can use cURL or HTTPie. The cURL is a Unix built-in utility that can be used to access the REST endpoints. HTTPie is a command-line HTTP client that allows you to access HTTP URLs. We’ll use this as an alternative to cURL to test our APIs. You can find more information on HTTPie at https://httpie.io/. You can also refer to http://mng.bz/KxeK for a quick introduction on installing and using HTTPie.

Let us start the application and access the endpoints. First, let’s create a course using the POST /courses/ endpoint. Listing 7.6 shows the HTTPie command to create a course.

Listing 7.6 The HTTPie command to create a new course

> http POST :8080/courses/ name="Mastering Spring Boot" rating=4
 category=Spring description="Mastering Spring Boot intends to teach
 Spring Boot with practical examples"
HTTP/1.1 200
// Other HTTP Response Headers
{
    "category": "Spring",
    "description": "Mastering Spring Boot intends to teach Spring Boot with
 practical examples",
    "id": 1,
    "name": "Mastering Spring Boot",
    "rating": 4
}

In listing 7.6, although we’ve supplied the request body data in key–value pair, the HTTPie tool internally converts it to a JSON payload. Once this command is executed in the terminal, a new course is created in the Course Tracker application. Let’s view the course details using the GET /courses/{id} endpoint to retrieve course details with a courseId obtained in the POST operation of listing 7.6. This is shown in the following listing.

Listing 7.7 The HTTPie command to view a course

> http GET :8080/courses/1
HTTP/1.1 200
// Other HTTP Response Headers
{
    "category": "Spring",
    "description": "Mastering Spring Boot intends to teach Spring Boot with
 practical examples",
    "id": 1,
    "name": "Mastering Spring Boot",
    "rating": 4
}

You can try accessing other endpoints in the same manner to monitor the output.

Discussion

With this technique, you’ve learned to create a complete RESTful API. We have kept the application extremely simple to demonstrate the concepts. Let’s now discuss a few best practices we’ve followed while designing the REST API.

If you notice, we’ve used JSON to accept the requests and similarly responded with JSON in the response. It is a best practice that the REST APIs accept request payloads in JSON and provide a response in JSON.

JSON is widely used to store and transfer data. Spring Boot provides built-in support to perform the mapping between JSON and Java POJOs and vice versa. For instance, if you notice in listing 7.6, you’ve sent a JSON request as the payload to create a new course in the application. However, the POST endpoint accepts a Course instance. Spring Boot performs this deserialization internally for us. By default, it uses the Jackson library (https://github.com/FasterXML/jackson) to perform this mapping.

The next thing to notice is the use of nouns while defining the endpoint paths. It is a best practice to use the plural form of the noun (e.g., Course, Person, Vehicle, etc.) to define the routes. We should not use verbs in the route paths as the HTTP request method already has a verb (e.g., GET, POST, etc.) that defines the actions. Letting the developers use the verbs in paths make the paths lengthy and inconsistent. For instance, to get the course details, one developer may use /getCourses, whereas another can use /retrieveCourses. However, the get or retrieve is already defined through the HTTP GET method. Thus, specifying it in the route path makes it redundant. Hence, GET /courses/ is the preferred endpoint path to get all courses. Similarly, the POST /courses/ is the appropriate endpoint to create a new course.

Figure 7.1 The communication flow diagram in a REST API. A user invokes a REST endpoint, which is handled by the REST Controller. The controller then uses the service layer to process the request. The service layer relies on the repository to communicate to the database. Once there is a response from the repository, it is processed by the service layer and forwarded to the controller. The controller may perform additional processing, and the final response is provided to the API client.

In listing 7.5, we’ve used the @RestController annotation in place of the previously used @Controller annotation. The @RestController annotation is a convenience annotation that is meta-annotated with the @Controller and @ResponseBody annotations. The @ResponseBody annotation indicates that a method’s return value should be bound to the HTTP response body.

Although the above API works well and serves its purpose, it has no exception handling. For instance, let’s try to delete a course that does not exist in the application. You’ll notice that you have been presented with an error and an ugly looking large stack trace. We’ll fix this in the next technique.

7.2 Managing exceptions in a Spring Boot RESTful API

Exceptions are inevitable in software code. Numerous factors could cause an exceptional scenario in your code. For instance, in the RESTful API we’ve designed, a user could attempt to access or delete a course with a nonexisting course ID. They could also submit a malformed JSON request payload to create a new course through the POST endpoint. All these scenarios cause exceptions in the API. In this section, we’ll discuss how to handle these exceptions and provide a meaningful response to the user specifying the exception details.

7.2.1 Technique: Handling exceptions in a RESTful API

In this technique, we’ll discuss how to handle exceptions in a RESTful API.

Problem

The previously defined RESTful API is unable to handle errors, as there is no exception handling in place. It presents the user with a large stack trace that is not intuitive and exposes application internal details. You need to handle exceptions and provide meaningful error responses.

Solution

Exception handling is an important aspect of a RESTful API. Typically, your APIs will be consumed by a variety of consumers and being able to provide a meaningful error response in the event of an exception scenario makes your API robust and user friendly.

Source code

The final version of the Spring Boot project is available at http://mng.bz/layj.

In the API designed in section 7.1, we’ve not handled the exceptions and the default Spring Boot exception handling mechanism is in place. For instance, deleting a course that does not exist in the application presents the error message, as shown in the following listing.

Listing 7.8 Default exception handling

C:sbip
epo>http DELETE :8080/courses/10
HTTP/1.1 500
{
    "error": "Internal Server Error",
    "message": "No class com.manning.sbip.ch07.model.Course entity with id
 10 exists!",
    "path": "/courses/10",
    "status": 500,
    "timestamp": "2021-06-23T16:38:20.105+00:00",
    "trace": "org.springframework.dao.EmptyResultDataAccessException: No
 class com.manning.sbip.ch07.model.Course entity with id 10
 exists!
	at
 org.springframework.data.jpa.repository.support.SimpleJpaRepository.lam
 bda$deleteById$0(SimpleJpaRepository.java:166)
	at
 java.base/java.util.Optional.orElseThrow(Optional.java:401)
	at
 org.springframework.data.jpa.repository.support.SimpleJpaRepository.del
 eteById(SimpleJpaRepository.java:165)
	at
 java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native
 Method)
	at
 java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMe
 thodAccessorImpl.java:64)
	at
 
// Remaining section of the exception is omitted

As you may notice, the above error message is not a desired one and contains details that are not of much use to the API users. It also exposes to the caller information about the tech stack used for the implementation of the API, which is generally considered a security flaw. Further, the HTTP response code is also generic (500 Internal Server Error), which indicates that a server-side error has occurred. In this technique, we’ll improve the Course Tracker RESTful API by implementing exception handling in the API.

To begin with, let’s first discuss the type of exceptions we may encounter in the application. For this API, we can have only a handful of exception scenarios. For instance, it may be possible that a user attempts to get, update, or delete a course that does not exist in the application. This should result in an HTTP 404 Not Found error, as the requested resource does not exist in the application. It is also possible that the user is submitting an incomplete/incorrect JSON payload, while creating or updating a course. Let’s handle these exception scenarios. This results in an HTTP 400 Bad Request status code, as the user request could not be processed because the server is unable to parse the request, since it is malformed. To handle the first scenario, let’s create a custom exception called CourseNotFoundException, as shown in the following listing.

Listing 7.9 CourseNotFoundException

package com.manning.sbip.ch07.exception;
 
public class CourseNotFoundException extends RuntimeException {
 
    private static final long serialVersionUID = 5071646428281007896L;
 
    public CourseNotFoundException(String message) {
        super(message);
    }
}

This CourseNotFoundException is thrown whenever API users attempt to access a course that does not exist in the application. Let’s now redefine the CourseServiceImpl class, as shown in the following listing.

Listing 7.10 CourseServiceImpl class

package com.manning.sbip.ch07.service;
//imports
 
@Service
public class CourseServiceImpl implements CourseService {

    // Additional Code
 
    @Override
    public Course updateCourse(long courseId, Course course) {
 
        Course existingCourse = courseRepository.findById(courseId)
        .orElseThrow(() -> new CourseNotFoundException(String.format("No
 course with id %s is available", courseId)));
        existingCourse.setName(course.getName());
        existingCourse.setCategory(course.getCategory());
        existingCourse.setDescription(course.getDescription());
        existingCourse.setRating(course.getRating());
        return courseRepository.save(existingCourse);
    }
 
 
    @Override
    public void deleteCourseById(long courseId) {
    courseRepository.findById(courseId).orElseThrow(() -> new
 CourseNotFoundException("No course with id %s is available" +
 courseId));
        courseRepository.deleteById(courseId);    }
}

Listing 7.10 shows the modified methods of CourseServiceImpl class. For an update or a delete operation, if a course with the supplied courseId does not exist in the application, we throw the CourseNotFoundException.

Now that we’ve thrown the exception, what’s next? We need to define an exception handler that intercepts the thrown exception and executes custom exception handling logic. For instance, for an unhandled exception, the HTTP response code 500 Internal Server Error is returned. However, if a course with the supplied courseid does not exist in the application, the appropriate HTTP error code should be 404 Not Found. The latter HTTP response code tells the API consumer the course they are accessing does not exist. Let’s define the GlobalExceptionHandler class that defines the ExceptionHandlers of our application, as shown in the following listing.

Listing 7.11 GlobalExceptionHandler class

package com.manning.sbip.ch07.exception.handler;
 
//imports
 
@ControllerAdvice
public class CourseTrackerGlobalExceptionHandler extends
 ResponseEntityExceptionHandler {
 
    @ExceptionHandler(value = {CourseNotFoundException.class})
    public ResponseEntity<?> handleCourseNotFound(CourseNotFoundException
 courseNotFoundException, WebRequest request) {
        return super.handleExceptionInternal(courseNotFoundException,
                courseNotFoundException.getMessage(), new HttpHeaders(),
 HttpStatus.NOT_FOUND, request);
    }
}

In the class in listing 7.11, you’ve defined a few ExceptionHandler implementations that handle the exceptions and can be thrown while processing the requests. Let’s explore this class in detail:

  • This class is annotated with the @ControllerAdvice annotation. This annotation is a specialized @Component that allows you to declare the @ExceptionHandler. The @ControllerAdvice annotation allows writing global code that applies to a range of controllers (and RestControllers). Thus, the ExceptionHandler defined in listing 7.11 applies to all controllers in the application.

  • This class extends the ResponseEntityExceptionHandler class, which is a base class for @ControllerAdvice annotated classes that provide a centralized exception handling across all @RequestMapping annotated methods through @ExceptionHandler methods. This class provides exception handling logic for a variety of exceptions that can occur in the application. We can extend this class and override the exception handling logic at our convenience.

  • We’ve defined a new ExceptionHandler for our custom exception CourseNotFoundException. In this implementation, we are setting the HTTP response code to 404 Not Found and the error message retrieved from the custom exception. Finally, we are invoking the superclass method handleExceptionInternal(..) with these details.

Let’s now start the application and try out replicating a few exceptions scenarios and observing the response. Let’s try deleting a course with a course ID that is not present in the application. The HTTPie command and the associated response is shown in the following listing.

Listing 7.12 Delete a course

C:sbip
epo>http DELETE :8080/courses/1
HTTP/1.1 404
// HTTP Response Headers
 
No course with id 1 is available

Notice that we have an appropriate HTTP status code 404 as well as a relevant error message that specifies the error. Moreover, the user does not see any reference to the technology used for the API implementation (i.e., no Spring Boot stack trace appearing anymore).

Discussion

The ability of a RESTful API to handle various user errors and to respond with appropriate HTTP status codes and error messages makes it robust and user friendly. This makes the application more compliant with the RESTful paradigm itself.

While designing APIs, it is a common practice to first identify the possible error scenarios in the application. You can then define custom exception classes that define the identified error scenario. One advantage of designing a custom exception is that it allows you to model the exception in a better manner and provides flexibility to capture various details about the exception. You can then define the ExceptionHandler that intercepts these exception classes and allows you to define custom error response. For instance, try defining an exception handler that handles the wrong request payloads and responds with the HTTP 400 bad request. We leave this as an exercise for the readers.

7.3 Testing a RESTful API

In the previous techniques, you’ve learned to design and build a RESTful API. Once you are done with the development, the next task is to test the endpoints of the API to ensure that the API is working as expected. There are multiple ways to test a REST API, as shown in figure 7.2.

Figure 7.2 Options to test a RESTful API. Command line utilities includes cURL, HTTPie. The GUI-based tools include Postman, SoapUI. Unit testing can be done with Spring Boot MockMVC in conjunction with JUnit.

So far, we’ve discussed using the command-line tool HTTPie that can be used to access the endpoints. You can also use the cURL utility to test the endpoints. If you are not comfortable with CLI utilities, GUI-based tools are another great alternative. In the REST API testing, Postman (https://www.postman.com/) is extensively used by API developers to test the APIs. Besides, if you are familiar with the Microsoft VS Code editor (https://code.visualstudio.com/), it also provides several extensions to enable testing support for the REST APIs. We won’t cover these utilities, as there are enough tutorials and how-to guides for these tools available on the internet.

In the next section, we’ll discuss how to test a REST API through integration testing. It is always a best practice to write test cases for the endpoints that are executed while you build the API. Let’s explore it in the next technique.

7.3.1 Technique: Testing a RESTful API in a Spring Boot application

In this section, we’ll explore how to test a RESTful API.

Problem

We haven’t defined any test cases to test the REST API endpoints. To ensure the API endpoints are working correctly and are not broken while introducing new changes in the future, we need to define integration test cases.

Solution

In a typical application, to test your application classes, you either instantiate those and invoke the methods defined in it or use mocking frameworks, such as Mockito to mock the class and other components. In a Spring MVC application, we can similarly define test cases. However, that does not verify a few important MVC framework features, such as request mapping, validation, data binding, @ExceptionHandler, and others.

Spring MVC provides a testing framework that provides comprehensive testing capabilities for Spring MVC-based applications without the need for an actual server. This framework, also known as MockMVC, performs the MVC request handling via mock request and response objects.

In this technique, we’ll show you how to use the Spring MockMVC framework in a Spring Boot application to test a REST API. We’ll define integration test cases for the API endpoints we’ve defined in the previous techniques.

Source code

The final version of the Spring Boot project is available at http://mng.bz/Bx4v.

Let’s begin by defining the first test case that creates a course in the Course Tracker application. The following listing shows the class.

Listing 7.13 Integration test case for Course Tracker REST API create course endpoint

package com.manning.sbip.ch07;
 
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static
 org.springframework.test.web.servlet.request.MockMvcRequestBuilders.del
 ete;
import static
 org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
 org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static
 org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static
 org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static
 org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonP
 ath;
import static
 org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import
 org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMo
 ckMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
import com.manning.sbip.ch07.model.Course;
import com.manning.sbip.ch07.service.CourseService;
 
@SpringBootTest
@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
class CourseTrackerApiApplicationTests {
 
    @Autowired
    private CourseService courseService;

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testPostCourse() throws Exception {
        Course course = Course.builder()
                .name("Rapid Spring Boot Application Development")
                .category("Spring")
                .rating(5)
                .description("Rapid Spring Boot Application
 Development").build();
        ObjectMapper objectMapper = new ObjectMapper();
 
        MockHttpServletResponse response = mockMvc.perform(post("/courses/")
                .contentType("application/json")
                .content(objectMapper.writeValueAsString(course)))
                .andDo(print())
                .andExpect(jsonPath("$.*", hasSize(5)))
                .andExpect(jsonPath("$.id", greaterThan(0)))
                .andExpect(jsonPath("$.name").value("Rapid Spring Boot
 Application Development"))
                .andExpect(jsonPath("$.category").value("Spring"))
                .andExpect(jsonPath("$.rating").value(5))
                .andExpect(status().isCreated()).andReturn().getResponse();
 
        Integer id =
 JsonPath.parse(response.getContentAsString()).read("$.id");
        assertNotNull(courseService.getCourseById(id));
 
    }
}

Let’s define various components we used in the class defined in listing 7.13:

  • The @SpringBootTest annotation indicates the annotated class runs Spring Boot-based tests and provides necessary environmental support to run the test cases. It creates the Spring application context that creates all Spring beans needed to run the test cases.

  • The @AutoConfigureMockMvc annotation enables and auto-configures the MockMVC framework. This annotation performs the heavy lifting to provide the necessary support, so we can simply autowire an instance of MockMVC and use it in the test.

  • The @ExtendWith(SpringExtension.class) annotation integrates the Spring TestContext Framework with JUnit 5’s Jupiter programming model. @ExtendWith is a JUnit 5 annotation that allows you to specify the extension to be used to run the test case.

  • We autowired the CourseService and the MockMvc instance in the class.

  • We used the mockMvc instance to perform an HTTP POST operation with a sample course.

Once the request is fired, we use the andExpect to assert various attributes. We’ve used the jsonpath to extract the values from the JSON response. Lastly, we validate the HTTP response status code. Let’s now provide the test case to get the course by ID. The following listing shows this test case.

Listing 7.14 Test case to get a course by a course ID

@Test
public void testRetrieveCourse() throws Exception {
     Course course = Course.builder()
                .name("Rapid Spring Boot Application Development")
                .category("Spring")
                .rating(5)
                .description("Rapid Spring Boot Application
 Development").build();
    ObjectMapper objectMapper = new ObjectMapper();
 
    MockHttpServletResponse response = mockMvc.perform(post("/courses/")
            .contentType("application/json")
            .content(objectMapper.writeValueAsString(course)))
            .andDo(print())
            .andExpect(jsonPath("$.*", hasSize(5)))
            .andExpect(jsonPath("$.id", greaterThan(0)))
            .andExpect(jsonPath("$.name").value("Rapid Spring Boot
 Application Development"))
            .andExpect(jsonPath("$.category").value("Spring"))
            .andExpect(jsonPath("$.rating").value(5))
            .andExpect(status().isCreated()).andReturn().getResponse();
    Integer id = JsonPath.parse(response.getContentAsString()).read("$.id");
 
    mockMvc.perform(get("/courses/{id}",id))
            .andDo(print())
            .andExpect(jsonPath("$.*", hasSize(5)))
            .andExpect(jsonPath("$.id", greaterThan(0)))
            .andExpect(jsonPath("$.name").value("Rapid Spring Boot
 Application Development"))
            .andExpect(jsonPath("$.category").value("Spring"))
            .andExpect(jsonPath("$.rating").value(5))
            .andExpect(status().isOk());
 
}

In listing 7.14, we’ve first created a course through the post() method and then used the get() method to retrieve the course details. Like the previous test case, we’ve asserted the various response parameters along with the HTTP response status code. Let’s now include the remaining test cases, as shown in the following listing.

Listing 7.15 Test cases for the Invalid Couse ID, Update, and Delete Course endpoints

@Test
public void testInvalidCouseId() throws Exception {
    mockMvc.perform(get("/courses/{id}",100))
    .andDo(print())
    .andExpect(status().isNotFound());
}
 
@Test
public void testUpdateCourse() throws Exception {
     Course course = Course.builder()
                .name("Rapid Spring Boot Application Development")
                .category("Spring")
                .rating(3)
                .description("Rapid Spring Boot Application
 Development").build();
    ObjectMapper objectMapper = new ObjectMapper();
 
    MockHttpServletResponse response = mockMvc.perform(post("/courses/")
            .contentType("application/json")
            .content(objectMapper.writeValueAsString(course)))
            .andDo(print())
            .andExpect(jsonPath("$.*", hasSize(5)))
            .andExpect(jsonPath("$.id", greaterThan(0)))
            .andExpect(jsonPath("$.name").value("Rapid Spring Boot
 Application Development"))
            .andExpect(jsonPath("$.category").value("Spring"))
            .andExpect(jsonPath("$.rating").value(3))
            .andExpect(status().isCreated()).andReturn().getResponse();
    Integer id = JsonPath.parse(response.getContentAsString()).read("$.id");
 
    Course updatedCourse = Course.builder()
            .name("Rapid Spring Boot Application Development")
            .category("Spring")
            .rating(5)
            .description("Rapid Spring Boot Application
 Development").build();

    mockMvc.perform(put("/courses/{id}", id)
            .contentType("application/json")
            .content(objectMapper.writeValueAsString(updatedCourse)))
            .andDo(print())
            .andExpect(jsonPath("$.*", hasSize(5)))
            .andExpect(jsonPath("$.id").value(id))
            .andExpect(jsonPath("$.name").value("Rapid Spring Boot
 Application Development"))
            .andExpect(jsonPath("$.category").value("Spring"))
            .andExpect(jsonPath("$.rating").value(5))
            .andExpect(status().isOk());
 
}
 
@Test
public void testDeleteCourse() throws Exception {
     Course course = Course.builder()
                .name("Rapid Spring Boot Application Development")
                .category("Spring")
                .rating(5)
                .description("Rapid Spring Boot Application
 Development").build();
   ObjectMapper objectMapper = new ObjectMapper();
 
   MockHttpServletResponse response = mockMvc.perform(post("/courses/")
           .contentType("application/json")
           .content(objectMapper.writeValueAsString(course)))
           .andDo(print())
           .andExpect(jsonPath("$.*", hasSize(5)))
           .andExpect(jsonPath("$.id", greaterThan(0)))
           .andExpect(jsonPath("$.name").value("Rapid Spring Boot
 Application Development"))
           .andExpect(jsonPath("$.category").value("Spring"))
           .andExpect(jsonPath("$.rating").value(5))
           .andExpect(status().isCreated()).andReturn().getResponse();
   Integer id = JsonPath.parse(response.getContentAsString()).read("$.id");
 
   mockMvc.perform(delete("/courses/{id}", id))
           .andDo(print())
           .andExpect(status().isOk());
 
}

In listing 7.15, we’ve defined three test cases:

  • The first test case attempts to get the course details for a course ID that is not available. The application returns an HTTP 404 status code, and we expect the same in the test case.

  • The second test case performs an HTTP PUT operation to test the update course endpoint.

  • The last test case performs the HTTP DELETE operation to delete a course with a courseId.

Discussion

Spring MockMVC framework provides an excellent way to test Spring MVC-based applications. Moreover, Spring Boot autoconfiguration of MockMVC has simplified defining the test cases even further. With this technique, we’ve demonstrated how to define test cases for the REST API endpoints with Spring’s MockMVC framework. The MockMVC framework provides a fluent API that allows you to perform the assertion of various response parameters. You can find further details regarding MockMVC at http://mng.bz/do5D.

Spring also provides an alternate test client called WebTestClient that lets you verify the response in a much better manner. We’ll demonstrate the use of WebTestClient in the next chapter.

7.4 Documenting a RESTful API

As part of modern-day application development, APIs play a critical role in the success of an application. As application features are consumed by a variety of devices, it is important that APIs are documented. Further, an API represents a contract between an API provider and consumers. Therefore, a good API should ensure that the API details are available to its consumers, so consumers can develop their code accordingly. These details include the HTTP request and response structure, HTTP status code that an endpoint returns, security configurations, and various other details. You can refer to https://petstore.swagger.io/ for a quick glimpse of the documentation of the Spring Petclinic application (https://github.com/spring-projects/spring-petclinic). In this section, we’ll discuss documenting the RESTful APIs through OpenAPI (https://swagger.io/specification/), which is the most popular and de facto standard of RESTful API documentation.

7.4.1 Technique: Documenting a RESTful API with OpenAPI

In this technique, we’ll learn how to document a RESTful API.

Problem

The Course Tracker API is currently undocumented, and there are no means other than exploring the application source code to find out the details regarding the API. We need to document this API with OpenAPI, so the API consumers can find the required details about the API.

Solution

The OpenAPI Specification provides a standard approach to document RESTful APIs, so the API consumers can find out the details and capabilities of the API in a consistent manner.

Source code

The final version of the Spring Boot project is available at http://mng.bz/raOg.

The OpenAPI specification is language-agnostic, which means it is not only limited to Spring Boot, but it is available for other languages and frameworks as well. For instance, we can use OpenAPI to document the RESTful API developed through a Spring Boot application, and the same is possible for a RESTful API developed through Express JS (https://expressjs.com/).

In this section, we’ll demonstrate how to document the Course Tracker API with OpenAPI. To proceed with that, let’s first add the following Maven dependency in the pom.xml file, as shown in the following listing.

Listing 7.16 OpenAPI Maven dependency

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.5.9</version>
</dependency>

The springdoc-openapi (https://springdoc.org/) library automates the generation of API documentation in a Spring Boot project. It does so by inspecting a Spring Boot application at runtime to infer the API semantics based on Spring configurations, class structure, and other annotations. The springdoc-openapi-ui dependency provides integration between Spring Boot and Swagger UI. It automatically deploys the swagger-ui to a Spring Boot application and makes it available at http://{server}:{port}/{context-path}/swagger-ui.html.

Notice that we’ve introduced Swagger in our discussion. Let’s clarify the difference between Swagger and OpenAPI. The OpenAPI is the specification that dictates the guidelines for the API documentation. Swagger is the tool that implements this specification. Swagger consists of various components, such as Swagger Editor, Swagger UI, Swagger Codegen, and a few other modules. Please refer to http://mng.bz/VlNX for a detailed discussion on Swagger vs. OpenAPI.

Let’s now proceed with documenting the Course Tracker API. To document the API, we annotate the endpoints with various annotations. These annotations contain custom details about the endpoint, such as the purpose of the endpoint, the HTTP status code it returns, and more. The following listing shows the updated CourseController annotated with the OpenAPI annotations.

Listing 7.17 The CourseController class

package com.manning.sbip.ch07.controller;
 
// imports
 
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
 
@RestController
@RequestMapping("/courses/")
@Tag(name = "Course Controller", description = "This REST controller 
 provide services to manage courses in the Course Tracker application")
public class CourseController {

    private CourseService courseService;

    @Autowired
    public CourseController(CourseService courseService) {
        this.courseService = courseService;
    }
 
    @GetMapping
    @ResponseStatus(code = HttpStatus.OK)
    @Operation(summary = "Provides all courses available in the Course 
 Tracker application")
    public Iterable<Course> getAllCourses() {
        return courseService.getCourses();
    }

    @GetMapping("{id}")
    @ResponseStatus(code = HttpStatus.OK)
    @Operation(summary = "Provides course details for the supplied course 
 id from the Course Tracker application")
    public Optional<Course> getCourseById(@PathVariable("id") long courseId) {
        return courseService.getCourseById(courseId);
    }

    @GetMapping("category/{name}")
    @ResponseStatus(code = HttpStatus.OK)
    @Operation(summary = "Provides course details for the supplied course 
 category from the Course Tracker application")
    public Iterable<Course> getCourseByCategory(@PathVariable("name")
String category) {
        return courseService.getCoursesByCategory(category);
    }

    @PostMapping
    @ResponseStatus(code = HttpStatus.CREATED)
    @Operation(summary = "Creates a new course in the Course Tracker 
 application")
    public Course createCourse(@Valid @RequestBody Course course) {
        return courseService.createCourse(course);
    }

    @PutMapping("{id}")
    @ResponseStatus(code = HttpStatus.NO_CONTENT)
    @Operation(summary = "Updates the course details in the Course Tracker 
 application for the supplied course id")
    public void updateCourse(@PathVariable("id") long courseId, @Valid
@RequestBody Course course) {
        courseService.updateCourse(courseId, course);
    }

    @DeleteMapping("{id}")
    @ResponseStatus(code = HttpStatus.NO_CONTENT)
    @Operation(summary = "Deletes the course details for the supplied 
 course id from the Course Tracker application")
    public void deleteCourseById(@PathVariable("id") long courseId) {
        courseService.deleteCourseById(courseId);
    }

    @DeleteMapping
    @ResponseStatus(code = HttpStatus.NO_CONTENT)
    @Operation(summary = "Deletes all courses from the Course Tracker 
 application")
    public void deleteCourses() {
        courseService.deleteCourses();
    }
 
}

In listing 7.17, we annotated the class with @Tag and the endpoints with @ResponseStatus and @Operation annotations. The @Tag provides information about the controller. The @ResponseStatus indicates the HTTP status code the endpoint returns. Notice that the HTTP status code is critical for the API consumer to code their application logic, as it defines the status of the API call. Thus, we must take care while determining the HTTP Status code for the endpoints. Lastly, the @Operation annotation captures details regarding the purpose of the endpoint.

Let’s now capture a few custom details about the API, such as API version, title, description, license details, and more. You can do this by defining a Spring bean of type OpenAPI. Listing 7.18 shows the OpenAPI bean definition. For simplicity, we’ve defined this bean in the Spring Boot main class, as shown in the following listing. In a typical application, you should define a separate Spring configuration class that should contain this @Bean definition.

Listing 7.18 The OpenAPI bean definition

package com.manning.sbip.ch07;
 
//imports
 
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
 
@SpringBootApplication
public class CourseTrackerApiApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(CourseTrackerApiApplication.class, args);
    }
 
    @Bean
    public OpenAPI customOpenAPI(@Value("${app.description}") String 
 appDescription,
            @Value("${app.version}") String appVersion) {
 
        return new OpenAPI().info(new Info().title("Course Tracker 
 API").version(appVersion)
                .description(appDescription).termsOfService("http:/ /swagger.io/terms/")
                .license(new License().name("Apache 
 2.0").url("http:/ /springdoc.org")));
 
    }
 
}

In listing 7.18, we defined the OpenAPI bean, which contains custom API details. In the following listing, we define the app.description and app.version properties in the application.properties file.

Listing 7.19 The application.properties file

app.description=Spring Boot Course Tracker API
app.version=v1

That’s all. Let’s start the application and access the swagger-ui to view the API documentation. You can access swagger-ui for this application at http://localhost:8080/ swagger-ui.html. Figure 7.3 shows the swagger-ui for the Course Tracker API.

Figure 7.3 The Course Tracker swagger documentation. It contains the API description, controller details, and endpoint details.

Discussion

OpenAPI is the de facto choice to document RESTful APIs. As you’ve seen in the previous example, by adding a few dependencies you have a nice HTML-based API document that captures the details about the API. However, one issue with the HTML is that it is difficult to share with the API consumers. To handle this, Swagger also lets you extract the API documentation in JSON format. You can retrieve this JSON by accessing the http://localhost:8080/v3/api-docs URL. This is shown in the following listing.

Listing 7.20 The API documentation in JSON format

{
   "openapi":"3.0.1",
   "info":{
      "title":"Course Tracker API",
      "description":"Spring Boot Course Tracker API",
      "termsOfService":"http:/ /swagger.io/terms/",
      "license":{
         "name":"Apache 2.0",
         "url":"http:/ /springdoc.org"
      },
      "version":"v1"
   },
   "servers":[
      {
         "url":"http:/ /localhost:8080",
         "description":"Generated server url"
      }
   ],
   "tags":[
      {
         "name":"Course Controller",
         "description":"This REST controller provides services to manage
 courses in the Course Tracker application"
      }
   ],
   "paths":{
      "/courses/{id}":{
         "get":{
            "tags":[
               "Course Controller"
            ],
 
// Remaining part of the JSON is omitted

Swagger provides the Swagger Editor (https://editor.swagger.io/), which allows you to import this JSON and renders the same HTML layout shown in figure 7.4.

Figure 7.4 Rendering the REST API documentation in the Swagger Editor. The Swagger Editor prefers the YAML version of the JSON data and automatically converts a JSON to YAML while you paste the JSON in the editor.

You can ship this JSON shown in listing 7.20 with API consumers to let them render it through Swagger Editor. To make life even simpler, Swagger also provides a Codegen utility that allows you to generate client applications from this JSON. For instance, let’s assume that the API client uses Node JS as their preferred language. You can generate this Node JS client stub with Swagger Codegen. Swagger Codegen also allows you to generate the client stub for a lot of different languages. Refer to https://swagger.io/tools/swagger-codegen/ for more details on Swagger Codegen. For further details on Spring Doc and OpenAPI integration, refer to Spring Doc reference documentation available at https://springdoc.org/.

7.5 Implementing RESTful API versioning

In this section, we’ll discuss the various approaches to versioning a RESTful API. However, before proceeding with the discussion of various versioning techniques, let’s discuss REST API versioning and why it’s necessary.

In simple words, versioning a REST API means the ability for the API to support multiple versions. It is a common occurrence to enhance or upgrade the application features over time. Various factors could drive these changes. For instance, it could be the implementation of new business features, adoption of a new technology stack, or refinement of the existing APIs.

However, the issue with a breaking API change is that it directly impacts the API consumers and breaks their application. It also causes a cascading impact on the API invocation chain. One way to resolve this issue is to implement versioning while designing your APIs. This way, you may have a version that is stable and available for your API consumers. For any breaking changes, you can introduce a newer version of the API that can be progressively adopted by various consumers.

In this section, we’ll discuss the available techniques to implement API versioning. Following is the list of techniques we’ll discuss in this chapter:

  • URI versioning—Uses a version number in the URI

  • Request parameter versioning—Uses an HTTP request parameter to identify the version

  • Custom HTTP header versioning—Uses an HTTP request header to distinguish the version

  • Media type versioning—Uses the accept header request header in the request to identify the version

We’ll demonstrate the different versioning techniques in the next technique. Later, we’ll provide an analysis on the merits and demerits of the approaches. To better explain the versioning techniques, we’ll simplify the CourseController class and only use the GET/courses/ and POST /courses/ endpoint for versioning. Let’s discuss this in the next technique.

7.5.1 Technique: Implementing versioning in a RESTful API

In this technique, we’ll discuss how to implement versioning in a RESTful API.

Problem

The Course Tracker API has not implemented any versioning strategy. We need to implement a versioning technique to ensure that the API can handle any breaking changes.

Solution

In this section, we’ll first discuss the URI versioning technique. This is a straightforward approach, as it includes a version identifier in the REST URI. For instance, /courses/v1 represents version 1 of the API, and /courses/v2 represents version 2 of the API.

Source code

The final version of the Spring Boot project is available at http://mng.bz/xv98.

Let’s assume we now need to enhance Course Tracker API, and it needs to also support an additional attribute of course price along with the previous course details. Introduction of course price could also mean that we can have additional REST endpoints, such as finding courses between a price range or retrieving courses based on the price order.

Note For simplicity reasons and demonstration purposes we are introducing the price attribute to the Course entity to design a new version of the API. In actual scenarios, there should be more appropriate reasons for API versioning.

To demonstrate this change, we’ll make changes to the CourseController class in the Course Tracker application. We’ll rename the existing CourseController class to LegacyCourseController and keep only GET /courses/ and POST /courses/ endpoints in it. The following listing shows the modified class.

Listing 7.21 The LegacyCourseController class

package com.manning.sbip.ch07.controller;
 
// imports
 
@RestController
@RequestMapping("/courses/v1")           
public class LegacyCourseController {

    private CourseService courseService;

    @Autowired
    public LegacyCourseController(CourseService courseService) {
        this.courseService = courseService;
    }

    @GetMapping
    @ResponseStatus(code = HttpStatus.OK)
    public Iterable<Course> getAllCourses() {
        return courseService.getCourses();
    }

    @PostMapping
    @ResponseStatus(code = HttpStatus.CREATED)
    public Course createCourse(@Valid @RequestBody Course course) {
        return courseService.createCourse(course);
    }
}

The request mapping URL contains the version number. We’ve appended version v1 to indicate the first version of the API.

The most notable change in listing 7.21 is that we’ve updated the @RequestMapping URI to /courses/v1. This is now the v1 version of the API. We’ll also introduce a new RestController called ModernCourseController. This controller class contains the changes related to the course price. The following listing shows the ModernCourseController class.

Listing 7.22 The ModernCourseController class

package com.manning.sbip.ch07.controller;
 
//imports
 
@RestController
@RequestMapping("/courses/v2")
public class ModernCourseController {

    private ModernCourseRepository modernCourseRepository;
    @Autowired
    public ModernCourseController(ModernCourseRepository
 modernCourseRepository) {
        this.modernCourseRepository = modernCourseRepository;
    }

    @GetMapping
    @ResponseStatus(code = HttpStatus.OK)
    public Iterable<ModernCourse> getAllCourses() {
        return modernCourseRepository.findAll();
    }

    @PostMapping
    @ResponseStatus(code = HttpStatus.CREATED)
    public ModernCourse createCourse(@Valid @RequestBody ModernCourse
 modernCourse) {
        return modernCourseRepository.save(modernCourse);
    }
}

Listing 7.22 represents the v2 version of the API, and we have done this by defining the @RequestMapping to /courses/v2 URI. We’ve also defined a new JPA entity class called ModernCourse that contains the new course attribute price along with other parameters and a new Spring Data repository interface called ModernCourseRepository available at http://mng.bz/Ax5z. For simplicity, we have skipped the service layer in the new version of the API.

That’s it. Now, let’s start the application and access both versions of the API. Listing 7.23 shows the output of creating and accessing a course with the v1 version of the API.

Listing 7.23 Creating and retrieving courses with v1 version of Courses Tracker API

>http POST :8080/courses/v1 name="Mastering Spring Boot" rating=4 
 category=Spring description="Mastering Spring Boot intends to teach 
 Spring Boot with practical examples"
HTTP/1.1 201
// Other HTTP Response Headers
 
{
    "category": "Spring",
    "description": "Mastering Spring Boot intends to teach Spring Boot with
 practical examples",
    "id": 1,
    "name": "Mastering Spring Boot",
    "rating": 4
}
 
 
>http GET :8080/courses/v1
HTTP/1.1 200
// Other HTTP Response Headers
 
[
    {
        "category": "Spring",
        "description": "Mastering Spring Boot intends to teach Spring Boot
 with practical examples",
        "id": 1,
        "name": "Mastering Spring Boot",
        "rating": 4
    }
]

Let’s now create and retrieve courses with the v2 version of the API. The following listing shows the output.

Listing 7.24 Creating and retrieving courses with v2 version of Course Tracker API

>http POST :8080/courses/v2 name="Mastering Spring Boot" rating=4 
 category=Spring description="Mastering Spring Boot intends to teach 
 Spring Boot with practical examples" price=42.34       
HTTP/1.1 201
// Other HTTP Response Headers
 
{
    "category": "Spring",
    "description": "Mastering Spring Boot intends to teach Spring Boot with
 practical examples",
    "id": 1,
    "name": "Mastering Spring Boot",
    "price": 42.34,
    "rating": 4
}
 
 
>http GET :8080/courses/v2
HTTP/1.1 200
// Other HTTP Response Headers
[
    {
        "category": "Spring",
        "description": "Mastering Spring Boot intends to teach Spring Boot
 with practical examples",
        "id": 1,
        "name": "Mastering Spring Boot",
        "price": 42.34,
        "rating": 4
    }
]

Creating a new course with the new version (/courses/v2) of the course API. Notice that we’ve included a new field named price in this endpoint.

As you may have noticed, both versions of the APIs are working fine. In the v1 version of the API, there is no price parameter. In the v2 version of the API, the price parameter is shown.

Let’s now discuss the second versioning technique of using an HTTP request parameter to determine the version. We’ll use the same Course Tracker application to demonstrate this versioning type.

Source code

The final version of the Spring Boot project is available at http://mng.bz/Zzdm.

For the HTTP request parameter-based versioning technique, you’ll provide a request parameter in the REST endpoint URI that dictates which version of the API should be invoked. Let’s define a new RestController class called RequestParameterVersioningCourseController. The following listing shows the RequestParameterVersioningCourseController class.

Listing 7.25 Implementing the versioning with HTTP request parameter

package com.manning.sbip.ch07.controller;
 
//imports
 
@RestController
@RequestMapping("/courses/")
public class RequestParameterVersioningCourseController {
 
      @Autowired
    private CourseService courseService;
 
      @Autowired
    private ModernCourseRepository modernCourseRepository;
 
    @GetMapping(params = "version=v1")
    @ResponseStatus(code = HttpStatus.OK)
    public Iterable<Course> getAllLegacyCourses() {
        return courseService.getCourses();
    }
 
    @PostMapping(params = "version=v1")
    @ResponseStatus(code = HttpStatus.CREATED)
    public Course createCourse(@Valid @RequestBody Course course) {
        return courseService.createCourse(course);
    }
 
    @GetMapping(params = "version=v2")
    @ResponseStatus(code = HttpStatus.OK)
    public Iterable<ModernCourse> getAllModernCourses() {
        return modernCourseRepository.findAll();
    }
 
    @PostMapping(params = "version=v2")
    @ResponseStatus(code = HttpStatus.CREATED)
    public ModernCourse createCourse(@Valid @RequestBody ModernCourse
 modernCourse) {
        return modernCourseRepository.save(modernCourse);
    }
}

In listing 7.25, notice the use of version=v1 and version=v2 request parameters that determines the endpoint to be invoked. Also notice that we’ve used the CourseService class for the v1 version of the API and ModernCourseRepository for the v2 version of the API. Ideally, we should define a service class to wrap the functionalities of the ModernCourseRepository interface for the version v2 API as well. For simplicity and demonstration purposes, we have skipped this step. In a real production application, you should define a service class for the controller.

You can start the application and access the new endpoints with the version=v2 parameter. The following listing shows the output.

Listing 7.26 Invoking the v2 version of POST /courses/ endpoint with request parameter

>http POST :8080/courses/?version=v2 name="Mastering Spring Boot" rating=4
 category=Spring description="Mastering Spring Boot intends to teach
 Spring Boot with practical examples" price=42.34
HTTP/1.1 201
// Other HTTP Response Headers
{
    "category": "Spring",
    "description": "Mastering Spring Boot intends to teach Spring Boot with
 practical examples",
    "id": 1,
    "name": "Mastering Spring Boot",
    "price": 42.34,
    "rating": 4
}
 
>http GET :8080/courses/?version=v2
// Other HTTP Response Headers
[
    {
        "category": "Spring",
        "description": "Mastering Spring Boot intends to teach Spring Boot
 with practical examples",
        "id": 1,
        "name": "Mastering Spring Boot",
        "price": 42.45,
        "rating": 4
    }
]

In the v1 version of the API, you’ll notice that the price parameter is not available.

Let’s now discuss the third API versioning technique that uses a custom HTTP header to identify the endpoint that needs to be invoked. This is quite similar to the second technique of using the HTTP request parameter. In this case, instead of an HTTP request parameter in the URI, we use a custom HTTP header in the HTTP request. Let’s define a new class that implements this versioning strategy.

Source code

The final version of the Spring Boot project is available at http://mng.bz/REjj.

Listing 7.27 shows the CustomHeaderVersioningCourseController class.

Listing 7.27 Implementing versioning with a custom HTTP header

package com.manning.sbip.ch07.controller;
// imports
 
@RestController
@RequestMapping("/courses/")
public class CustomHeaderVersioningCourseController {
 
    private CourseService courseService;
    private ModernCourseRepository modernCourseRepository;
 
    @Autowired
    public CustomHeaderVersioningCourseController(CourseService
 courseService, ModernCourseRepository modernCourseRepository) {
        this.courseService = courseService;
        this.modernCourseRepository = modernCourseRepository;
    }
 
    @GetMapping(headers = "X-API-VERSION=v1")
    @ResponseStatus(code = HttpStatus.OK)
    public Iterable<Course> getAllLegacyCourses() {
        return courseService.getCourses();
    }
 
    @PostMapping(headers = "X-API-VERSION=v1")
    @ResponseStatus(code = HttpStatus.CREATED)
    public Course createCourse(@Valid @RequestBody Course course) {
        return courseService.createCourse(course);
    }
 
    @GetMapping(headers = "X-API-VERSION=v2")
    @ResponseStatus(code = HttpStatus.OK)
    public Iterable<ModernCourse> getAllModernCourses() {
        return modernCourseRepository.findAll();
    }
 
    @PostMapping(headers = "X-API-VERSION=v2")
    @ResponseStatus(code = HttpStatus.CREATED)
    public ModernCourse createCourse(@Valid @RequestBody ModernCourse
 modernCourse) {
        return modernCourseRepository.save(modernCourse);
    }
}

In listing 7.27, we used a custom HTTP header X-API-VERSION to determine the endpoint that needs to be invoked. To invoke a REST endpoint, you need to supply the X-API-VERSION header in your HTTP request. The following listing shows the use of this custom HTTP header.

Listing 7.28 Invoking the v2 version of POST /courses/ endpoint with a custom HTTP header

>http POST :8080/courses/ X-API-VERSION:v2 name="Mastering Spring Boot"
 rating=4 category=Spring description="Mastering Spring Boot intends to
 teach Spring Boot with practical examples" price=42.34
HTTP/1.1 201
// Other HTTP Response Headers
 
{
    "category": "Spring",
    "description": "Mastering Spring Boot intends to teach Spring Boot with
 practical examples",
    "id": 1,
    "name": "Mastering Spring Boot",
    "price": 42.34,
    "rating": 4
}
 
>http GET :8080/courses/ X-API-VERSION:v2
 
// Other HTTP Response Headers
 
[
    {
        "category": "Spring",
        "description": "Mastering Spring Boot intends to teach Spring Boot
 with practical examples",
        "id": 1,
        "name": "Mastering Spring Boot",
        "price": 42.34,
        "rating": 4
    }
]

The last versioning technique we’ll discuss in this section is media-type versioning. This is also known as the Content Negotiation or Accept Header versioning strategy. This is due to the use of the Accept HTTP request header. In this technique, instead of using a custom HTTP header, we leverage the built-in Accept HTTP header. With the Accept HTTP header, a client indicates a server the content types (through MIME types) that the client understands. In the HTTP request, the client provides the Accept header. In the content negotiation (http://mng.bz/2jB8) phase, the server uses its internal algorithm to determine one of the Accept header values and inform the choice with the Content-Type response header.

Source code

The final version of the Spring Boot project is available at http://mng.bz/1jl1.

Let’s define the AcceptHeaderVersioningCourseController class that implements the versioning technique with the Accept HTTP header. This implementation is shown in the following listing.

Listing 7.29 Implementing the versioning with Accept HTTP header

package com.manning.sbip.ch07.controller;
 
//imports
 
 
@RestController
@RequestMapping("/courses/")
public class AcceptHeaderVersioningCourseController {
 
    private CourseService courseService;
    private ModernCourseRepository modernCourseRepository;
 
    @Autowired
    public AcceptHeaderVersioningCourseController(CourseService
 courseService, ModernCourseRepository modernCourseRepository) {
        this.courseService = courseService;
        this.modernCourseRepository = modernCourseRepository;
    }
 
    @GetMapping(produces = "application/vnd.sbip.app-v1+json")
    @ResponseStatus(code = HttpStatus.OK)
    public Iterable<Course> getAllLegacyCourses() {
        return courseService.getCourses();
    }
 
    @PostMapping(produces = "application/vnd.sbip.app-v1+json")
    @ResponseStatus(code = HttpStatus.CREATED)
    public Course createCourse(@Valid @RequestBody Course course) {
        return courseService.createCourse(course);
    }
 
    @GetMapping(produces = "application/vnd.sbip.app-v2+json")
    @ResponseStatus(code = HttpStatus.OK)
    public Iterable<ModernCourse> getAllModernCourses() {
        return modernCourseRepository.findAll();
    }
 
    @PostMapping(produces = "application/vnd.sbip.app-v2+json")
    @ResponseStatus(code = HttpStatus.CREATED)
    public ModernCourse createCourse(@Valid @RequestBody ModernCourse
 modernCourse) {
        return modernCourseRepository.save(modernCourse);
    }
}

In the following listing, we’ve used the produces attribute of the @GetMapping and @PostMapping annotations that declares the content the endpoint produces. The application/vnd.sbip.app-v1+json is a custom MIME type that indicates the v1 version of the API, and application/vnd.sbip.app-v2+json specifies the v2 version of the API. The following listing shows the use of the Accept HTTP header.

Listing 7.30 Invoking the v2 version of POST /courses/ endpoint with Accept HTTP header

>http POST :8080/courses/ Accept:application/vnd.sbip.app-v2+json
 name="Mastering Spring Boot" rating=4 category=Spring
 description="Mastering Spring Boot intends to teach Spring Boot with
 practical examples" price=42.34
HTTP/1.1 201
Connection: keep-alive
Content-Type: application/vnd.sbip.app-v2+json
Date: Fri, 25 Jun 2021 18:42:15 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked
 
{
    "category": "Spring",
    "description": "Mastering Spring Boot intends to teach Spring Boot with
 practical examples",
    "id": 1,
    "name": "Mastering Spring Boot",
    "price": 42.34,
    "rating": 4
}
 
>http GET :8080/courses/ Accept:application/vnd.sbip.app-v2+json
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/vnd.sbip.app-v2+json
Date: Mon, 08 Nov 2021 02:39:29 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked
 
[
    {
        "category": "Spring",
        "description": "Mastering Spring Boot intends to teach Spring Boot
 with practical examples",
        "id": 1,
        "name": "Mastering Spring Boot",
        "price": 42.34,
        "rating": 4
    }
]

Discussion

With this technique, we’ve seen the various techniques to implement versioning in a REST API. Now that you have several choices to implement versioning, the immediate next question that comes to mind is which approach is better and preferable. This is a difficult question, and there is no straightforward answer to it. This is because none of the solutions we’ve discussed are perfect.

For instance, many developers reject the idea of assigning a version number in the endpoint URI, as it creates URI pollution. Since the version is not part of the actual URI, many argue the presence of the version identifier is a bad practice. Versioning in the URI exposes to the API consumers that there are multiple versions of the API that exist. Many organizations do not expose this fact to the API consumers.

Similarly, many developers reject the idea of using Accept header for versioning purposes, as the Accept HTTP header is not designed for this purpose. Using Accept header for versioning is just a workaround and is not considered a preferred solution to implement versioning. A similar type of counterargument is available for the other two versioning techniques.

If there are multiple versions of the same endpoint available, it causes issues while documenting the API. For instance, the API consumers may get confused if they find two different approaches to invoke the same service.

As you may notice, there are both merits and demerits of the discussed approaches. Thus, selecting a versioning strategy is a design choice of API designers or the organizations after analyzing the pros and cons of the approach before adopting it to practice. The following list shows the API versioning approaches adopted by several major API providers:

  • Amazon—Request parameter versioning

  • GitHub—Media type versioning

  • Microsoft—Custom header versioning

  • Twitter—URI versioning

7.6 Securing a RESTful API

In previous sections, we discussed various aspects of developing a RESTful API that includes developing an API, its documentation, its testing, and its versioning. We are still left with another core aspect of API development. And it’s the security of the API. Presently, this API is not secure, and anyone who knows the API endpoints can access the API.

There are several ways an API can be secured. The most straightforward approach is using HTTP basic authentication to secure the API. This is the simplest one to implement, as it uses a username and password to authenticate the users. You may remember that in chapter 5, we demonstrated how to implement HTTP basic authentication to secure a Spring Boot application. You can refer to section 5.3.6 in chapter 5 to implement HTTP basic authentication in the Course Tracker API.

However, you should limit the use of HTTP basic authentication to the extent possible due to its various shortcomings. Only consider it for your internal testing or development purposes. An attentive reader may ask why we are discussing it here if it is not recommended to use. The use of basic authentication is still widespread (http://mng.bz/PWKY) due to its simplicity and ease of use. Only recently, some organizations are deprecating the use of this authentication strategy (http://mng.bz/J1pK).

Let’s discuss the reasons we should not use it in a production application in the first place. First, HTTP basic authentication uses the username and password in plain-text format with Base64 encoding to authenticate the users. The Base64 encoding is not an encryption technique, and it is extremely easy to retrieve the credentials from a Based64 encoded string. Thus, without HTTPS, there is a high chance credentials could be exposed. Second, with HTTP basic authentication technique, both the client application and the server application act as the password keeper and manage the user credentials for authentication and authorization purposes. This is again problematic, as there are chances that credentials could be compromised by either party.

A preferred approach would be managing the user credentials in a centralized authorization server instead of allowing either the server or the client application to deal with the user password. The authorization server can issue a token that could be used for authentication and authorization purposes. Let’s discuss this approach in the next technique.

7.6.1 Technique: Using JWT to authorize RESTful API requests

In this technique, we will discuss how to authorize RESTful API requests using JWT.

Problem

The Course Tracker RESTful API has not implemented any security measures that can secure the REST endpoints. Without security configurations, anyone can access the application endpoints.

Solution

With this technique, we’ll demonstrate how to secure the endpoint access with the Bearer Token approach. As mentioned previously, we’ll use an authorization server for authorizing access. However, before proceeding with the implementation, let’s provide a high-level overview of the REST request and response flow between the client, REST API server, and the authorization server. Figure 7.5 shows a block diagram of this flow:

Figure 7.5 The communication between client application, REST API server, and the authorization server to access a REST endpoint by a client. We are using the OAuth2 framework for authentication and authorization.

Let’s understand the flow discussed in the figure:

  • A client requests to get course details from the Course Tracker REST API by invoking the GET /courses endpoint.

  • As the client is not authenticated, the API responds with 401 Unauthorized and indicates in the HTTP response header that it needs to authenticate itself with a Bearer Token.

  • The client then requests the authorization server to get a Bearer Token. While making this request, the client supplied the required details, such as client_id, username, password, scope, and others. Note that the user for which the Bearer Token is requested needs to be configured before a token is requested.

  • For a valid token request, the authorization server returns an access_token in JSON Web Token (JWT) format.

  • The client application makes a new request to the Course Tracker REST API and supplies the Bearer token in the request.

  • The Course Tracker REST API validates the token with the authorization server and receives a response.

  • For a valid response, the Course Tracker REST API returns the requested course details. For an invalid response from the authorization server, it returns an error response to the client.

Note that the flow in figure 7.5 is for a new request if the API client attempts to access the endpoint without supplying the JWT token. If the client is supplying the token, the communication starts at step 5.

Source code

The final version of the Spring Boot project is available at http://mng.bz/wn72.

Let’s now begin with the implementation. The first thing that needs to be done is to configure the authorization server. We’ll use Keycloak (https://www.keycloak.org/) as the authorization server. We’ll configure two users, namely john and steve, in the authorization server. You can refer to the following GitHub wiki http://mng.bz/q27J to set up the authorization server. It is strongly recommended that you set up the authorization server before continuing with the next steps.

To keep the example simple, we’ve simplified the Course Tracker application a bit. The course domain entity now contains only three fields: a course ID, a name, and an author. The following listing shows the updated course class.

Listing 7.31 The updated course entity

package com.manning.sbip.ch07.model;
 
// imports
 
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Course {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    private Long id;
 
    @NotEmpty
    @Column(name = "NAME")
    private String name;
 
    @NotEmpty
    @Column(name = "AUTHOR")
    private String author;
}

We’ve also simplified the CourseController class, and it has the following endpoints:

  • Get courses by an author.

  • Get course by an ID.

  • Create a new course.

  • Update an existing course.

To enable JSON Web Token (JWT) support, we need to update the pom.xml with the dependencies shown in the following listing.

Listing 7.32 The Maven depencies for OAuth2 and JWT support

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

The first dependency makes the Course Tracker application an OAuth2 resource server. The second dependency provides support for JWT (https://jwt.io/introduction). Let’s now include the property in the application.properties file shown in the following listing.

Listing 7.33 The JSON Web Token issues URL

spring.security.oauth2.resourceserver.jwt.issuer-
 uri=http:/ /localhost:9999/auth/realms/master

Listing 7.33 configures the Keycloak JWT issuer URL. Let’s now explore the updated CourseController class, as shown in the following listing.

Listing 7.34 The updated CourseController class

package com.manning.sbip.ch07.controller;
 
//imports
 
@RestController
@RequestMapping("/courses/")
public class CourseController {

    private CourseRepository courseRepository;

    @Autowired
    public CourseController(CourseRepository courseRepository) {
        this.courseRepository = courseRepository;
    }
   
    @GetMapping
    public Iterable<Course> getAllCourses(@AuthenticationPrincipal Jwt 
 jwt) {                                                               
        String author = jwt.getClaim("user_name");
        return courseRepository.findByAuthor(author);
    }

    @GetMapping("{id}")
    public Optional<Course> getCourseById(@PathVariable("id") long courseId) {
        return courseRepository.findById(courseId);
    }

    @PostMapping
    public Course createCourse(@RequestBody String name,
 @AuthenticationPrincipal Jwt jwt) {
        Course course = new Course(null, name, jwt.getClaim("user_name"));
        return courseRepository.save(course);
    }
}

The user_name is a custom claim defined in the authorization server. In this context, we use it to get the author name to look up the courses authored by a user.

In listing 7.34, we used the @AuthenticationPrincipal annotation to get access to the JWT token. This JWT instance contains the various details about the user request. From the JWT, we retrieve the user_name claim, which is the course author name in this context. Let’s now create two courses: one for the author john and another for author steve, as shown in the following listing.

Listing 7.35 Creating courses in the application

@Bean
CommandLineRunner createCourse(CourseRepository courseRepository) {
    return (args) -> {
        Course spring = new Course(null, "Spring", "john");
        Course python = new Course(null, "Python", "steve");
        courseRepository.save(spring);
        courseRepository.save(python);
    };
}

That’s all. Let’s now start the application and try accessing the endpoints. Listing 7.36 shows the outcome while we try to access the GET /courses/ endpoint without supplying a JWT token.

Listing 7.36 Accessing GET /courses/ endpoint without a JWT token

>http GET :8080/courses
HTTP/1.1 401
WWW-Authenticate: Bearer
// HTTP Response Headers

The request is denied with an HTTP 401 unauthorized error response. The API has also responded with the WWW-Authenticate:Bearer response header indicating the client needs to provide a Bearer Token in the HTTP request. This is automatically done by Spring Security. As we are using Bearer Token-based authentication, Spring Security uses the BearerTokenAuthenticationFilter to process the incoming request. It attempts to parse the request and generates a JwtAuthenticationToken, which contains the JWT token details. In the discussion section, we’ll provide more details on the classes used to process the request. For now, remember that the BearerTokenAuthenticationFilter is the Spring Security filter that performs the authentication. Let’s now try to obtain a Bearer Token for the user john, so the same can be included in the HTTP request. The following listing shows the command to obtain a token.

Listing 7.37 Obtaining a JWT from the Keycloak authorization server

C:Usersmusib>http --form POST http:/ /localhost:9999/auth/realms/master/protocol/openid-connect/token 
 grant_type=password client_id=course-tracker scope=course:read 
 username=john password=password Content-Type:application/x-www-form-
 urlencoded
HTTP/1.1 200 OK
// HTTP Response Headers
 
{
    "access_token": 
 "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJxY2lKalIxSWNocTk4Qk
 VMcEo5cDJiWDBRaF80MzZ1S0ktbkx4UlF3Zk53In0.eyJleHAiOjE2MjQ3NzczOTgsImlhd
 CI6MTYyNDc3Mzc5OCwianRpIjoiYTY4OWM0Y2ItYTVhZC00YTM5LWE1YjQtNjFjNGNhNGZk
 MjMzIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5OTk5L2F1dGgvcmVhbG1zL21hc3RlciI
 sImF1ZCI6ImNvdXJzZS10cmFja2VyIiwic3ViIjoiNmQxMTE4MTktZmFlZC00NzQzLWFiNT
 EtMzk0YmVmNGQ0ZjBlIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiY291cnNlLXRyYWNrZXIiL
 CJzZXNzaW9uX3N0YXRlIjoiOWIyMTdiOTUtOWM1MS00ZGY0LWI3NTYtYTI3NzdmNmI0MDk2
 IiwiYWNyIjoiMSIsInNjb3BlIjoiY291cnNlOndyaXRlIGNvdXJzZTpyZWFkIiwidXNlcl9
 uYW1lIjoiam9obiIsImF1dGhvcml0aWVzIjpbInVzZXIiXX0.NgBcrpPvDB36sd2ytaeMUk
 qM_1_psUDMsHHkB9zZlT_9sIwF3kdPOhSLSmoMqhFtGpOOJI5CmB92WEBu4rVcNa2lnuh16
 lkksnC-0ASn23z8TIRtucrQ-
 Px2bOgFyducmRH7ec93gOsLKeZSUnjup0YA9FT_0o7eroKFdWrrqoyOiAxOua9nGg307Lkv
 _VKXtCB5wSrPFfPQrp6muw-gcREJaBgcYSx-
 5QKC5UK30cFSsWlKXC9i2ov2O3aPA4DlHIqWx06a_M7AKmvgG3fVpyJSztbi0XHDnU9Y_mJ
 Vug-WH5MOIpgRUmYYnSL1Ki3PV24tZ11LolyA13XsA859vg",
    "expires_in": 3600,
    "not-before-policy": 0,
    "refresh_expires_in": 1800,
    "refresh_token":
 "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyYzI4MTNiNy05NmIzLT
 RkMzctYmUwOS1lMTE0ZTkzZjJlNTcifQ.eyJleHAiOjE2MjQ3NzU1OTgsImlhdCI6MTYyND
 c3Mzc5OCwianRpIjoiMTU4Y2E1ZGQtMDMyNy00NTE4LTk4NWItZGQ5ZTliNzcwNjg5Iiwia
 XNzIjoiaHR0cDovL2xvY2FsaG9zdDo5OTk5L2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6
 Imh0dHA6Ly9sb2NhbGhvc3Q6OTk5OS9hdXRoL3JlYWxtcy9tYXN0ZXIiLCJzdWIiOiI2ZDE
 xMTgxOS1mYWVkLTQ3NDMtYWI1MS0zOTRiZWY0ZDRmMGUiLCJ0eXAiOiJSZWZyZXNoIiwiYX
 pwIjoiY291cnNlLXRyYWNrZXIiLCJzZXNzaW9uX3N0YXRlIjoiOWIyMTdiOTUtOWM1MS00Z
 GY0LWI3NTYtYTI3NzdmNmI0MDk2Iiwic2NvcGUiOiJjb3Vyc2U6d3JpdGUgY291cnNlOnJl
 YWQifQ.a1O4SuspoN5u_RvYdXZsb6WLC3INx1smroEIVdYWG_E",
    "scope": "course:write course:read",
    "session_state": "9b217b95-9c51-4df4-b756-a2777f6b4096",
    "token_type": "Bearer"
}

In listing 7.37, we used the Keycloak authorization server’s token endpoint with the required parameters. If you recall, we’ve configured all the attributes in the command while setting up and configuring the client application and the users in the Keycloak server. Revisit the GitHub wiki link to understand the purpose of these parameters. Let’s explain the various request parameters we’ve used to access the token details:

  • We have used x-www-form-urlencoded as the content type, since the Keycloak server understands this request.

  • The grant_type refers to how an application gets an access token. The grant_ type=password tells the token endpoint that the application is using the Password grant type.

  • A client_id is generated in the authorization server once an application is registered in the server.

  • Scope refers to one or more space-separated strings indicating which permission the application is requesting. In this case, the scope value we are requesting is course:read.

  • The username and password fields supply the username and password of the user.

In the HTTP response, the Keycloak server returns the access_token and the client scopes configured for the user and token_type. For now, we’ll use the access_token from this response to include this token in the HTTP GET request to the Course Tracker API. Note that we’ve configured the access token to be valid for one hour. Typically, in a production application, tokens are configured to be short-lived for security reasons. For simplicity and testing purposes, we’ve configured the token to be valid for one hour. In your testing, you should generate a new token and should not use the token provided in listing 7.37. The following listing shows the HTTP GET /courses/ request with the token.

Listing 7.38 Accessing GET /courses/ endpoint with a JWT token

C:Usersmusib>http GET :8080/courses/ "Authorization:Bearer
 eyJhbGciOiJSUzI1NiIsInR5cCIgOiAi..."        
HTTP/1.1 200
// HTTP Response Headers
[
    {
        "author": "john",
        "id": 1,
        "name": "Spring"
    }
]

For brevity and readability purposes, we’ve elided the complete token details.

This time the HTTP status code is 200 OK, and we can retrieve the courses authored by user john.

Although this approach works well, there is a flaw in the implementation. With the current security implementation, we can use the token of one user to get details of the other users. For instance, in this case, we can use the token of john to access the courses authored by steve, as shown in the following lisitng.

Listing 7.39 Accessing author Steve’s course details with author John’s token

>http GET :8080/courses/2 "Authorization:Bearer
 eyJhbGciOiJSUzI1NiIsInR5cCIgOiAi..."
HTTP/1.1 200
// HTTP Response Headers
 
{
    "author": "steve",
    "id": 2,
    "name": "Python"
}

Ouch! We can access author Steve’s course details (which is course ID 2) with the token of author John. This is an access control issue in the application known as the insecure direct object reference (IDOR) problem (see http://mng.bz/7WBe).

This problem occurred because the token for user john is a valid token, and the endpoint GET /courses/{id} is not performing any access control check. To avoid this issue, we’ll implement method level security with Spring Security. Simply put, the method level security allows you to secure the methods. We’ll leverage the Spring Security @PreAuthorize or @PostAuthorize annotations to implement this. These annotations take Spring Expression Language (SpEL) expression, which is evaluated to make the access control decisions.

Let’s demonstrate the use of the @PostAuthorize annotation to prevent the Insecure Direct Object Reference problem. The access problem happened because there were no checks for whether the supplied token belongs to the author requesting access to the course details performed at the endpoint (with the supplied course ID). We can retrieve the author name (using the user_name claim) from the token and compare it with the returned course author name. If there is a mismatch, then we’ll forbid this access.

To use the method level security, you need to include the @EnableGlobalMethodSecurity(prePostEnabled = true) in the Spring Boot main class. This annotation enabled the method level security in the application, as shown in the following listing.

Listing 7.40 Configuring the EnableGlobalMethodSecurity annotation

package com.manning.sbip.ch07;
 
import
 org.springframework.security.config.annotation.method.configuration.Ena
 bleGlobalMethodSecurity;
 
//Other imports
 
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CourseTrackerApiApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(CourseTrackerApiApplication.class, args);
    }
}

Next, you need to include the @PostAuthorize annotation on the offending endpoint. The following listing shows the updated endpoint.

Listing 7.41 Implementing @PostAuthorize to secure access control

@GetMapping("{id}")
@PostAuthorize("@getAuthor.apply(returnObject, 
 principal.claims['user_name'])")
public Optional<Course> getCourseById(@PathVariable("id") long courseId) {
    return courseRepository.findById(courseId);
}

We supplied two attributes to a BiFunction implementation that performs the comparison of the token-supplied author name and the method-returned author name and returns a Boolean value. We’ve supplied the SpEL expression @getAuthor .apply(returnObject, principal.claims['user_name']) to perform the access control. The returnObject is the method return object, which is Optional<Course>, and the principal.claims['user_name']) provides the author name. Listing 7.42 shows this BiFunction implementation as a bean definition in the Spring Boot main class. For simplicity, we’ve included this @Bean definition in the Spring Boot main class. In a real application, define a Spring configuration class to define this bean.

Listing 7.42 The BiFunction implementation

@Bean
BiFunction<Optional<Course>, String, Boolean> getAuthor() {
    return (course, userId) -> course.filter(c ->
 c.getAuthor().equals(userId)).isPresent();
}

Let’s again try accessing course ID 2 with the access token of author john. The following listing shows the outcome.

Listing 7.43 Accessing author Steve’s course details with author John’s token

C:Usersmusib>http GET :8080/courses/2 "Authorization:Bearer
 eyJhbGciOiJSUzI1NiIsInR5cCIgOiAi.."
HTTP/1.1 403
// HTTP Response Headers

We ended up with the 403 Forbidden HTTP status code. The 403 HTTP return code indicates that the requested user was successfully authenticated to the application but failed in the authorization while accessing the endpoint.

The next thing we’ll discuss in this technique is the use of a scope to perform access control in the application. For instance, we can use a scope called course:read to ensure that tokens with this scope can access an endpoint.

A scope defines the access level provided in the token to a client application by a user. Imagine, you (as the user) have granted access to a third-party client application to read all the courses authored by you, but you want to restrict that the client application should not be able to perform any write operation. Thus, you can grant (through grant_type=password) the third-party client application to obtain a token (by accessing the Keycloak server) only with the course:read scope. If the application attempts to perform a write operation for any reason, it will receive a 403 Forbidden error, as the write operation requires a different scope (e.g. course:write), which is not provided while granting the token.

We’ll use the @PreAuthorize annotation to implement this. Let’s add the following annotation in the getCourseById(..) method to the CourseController class, as shown in the following listing.

Listing 7.44 Implementing the scope-based access control

@GetMapping("{id}")
@PreAuthorize("hasAuthority('SCOPE_course:read')")
@PostAuthorize("@getAuthor.apply(returnObject,
 principal.claims['user_name'])")
public Optional<Course> getCourseById(@PathVariable("id") long courseId) {
    return courseRepository.findById(courseId);
}

Spring Security appends the SCOPE_ prefix in the scope. Thus, we’ve configured the course:read scope as SCOPE_course:read. The @PreAuthorize annotation checks whether the requester (the client application) has the defined scope and, based on that, decides the access. We leave it as an exercise to the reader to play around with the Keycloak server to configure various scopes and explore the access control outcomes.

Discussion

In this technique, you’ve explored using JWT with an authorization server to secure REST endpoints. Explaining the OAuth2 and the authorization server in depth is beyond the scope of this text. You can refer to books dedicated to OAuth2 (https://www.manning.com/books/oauth-2-in-action), OpenID connect (https://www.manning.com/books/openid-connect-in-action), and Spring Security (https://www.manning.com/books/spring-security-in-action) for a better understanding of these subjects.

In chapter 5, we demonstrated the use of Spring Security to secure Spring Boot applications. We also discussed that Spring Security uses a FilterChain and a list of filters that enforces security in the application. For Bearer Token-based authentication, Spring Security provides BearerTokenAuthenticationFilter. Figure 7.6 shows the flow of how the JWT is processed and a final JwtAuthenticationToken is generated.

Figure 7.6 The list of classes and the flow to process a JWT and generate a JwtAuthenticationToken

The BearerTokenAuthenticationFilter delegates the JWT processing to an AuthenticationManager to perform the authentication. The AuthenticationManager uses JwtAuthenticationProvider to perform the actual authentication task. It uses a JwtDecoder and JwtAuthenticationConverter that process the request and generate the JwtAuthenticationToken.

Summary

Let’s summarize the key takeaways of this chapter:

  • We developed a RESTful API with Spring Boot application and discussed a few best practices for developing an API.

  • We explored how to perform exception handling and provide appropriate HTTP response codes.

  • We explored the use of OpenAPI to document a REST API.

  • We explored various techniques to implement versioning in a REST API. The techniques we discussed are URI versioning, request parameters, custom headers, and Accept header-based versioning.

  • We implemented Bearer Token-based authentication and authorization techniques to secure the REST API.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.230.82