Building a REST API with Spring Boot
Module 2: Developing a Secure App
Lesson: Get List
Send pageable request to Spring Data.
interface CashCardRepository extends CrudRepository<CashCard, Long>, PagingAndSortingRepository<CashCard, Long> {
}
private final CashCardRepository cashCardRepository;
@GetMapping
private ResponseEntity<List<CashCard>> findAll(Pageable pageable) {
Page<CashCard> page = cashCardRepository.findAll(
PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
pageable.getSortOr(Sort.by(Sort.Direction.ASC, "amount"))
));
return ResponseEntity.ok(page.getContent());
}
Lesson: Simple Spring Security
Authentication
-
Principal is a synonym for user that can be a person or another program.
-
Authentication is the act of a Principal providing its identity to the system, ex. providing credentials (username and password). Upon providing proper credentials, the Principal is authenticated, i.e. successfully logged in.
-
Authentication Session is created once a Principal gets authenticated and can be implemented in many ways, ex. which can be implemented via Session Token placed in a Cookie.
-
Cookie is a set of data stored in a web client (browser) and associated with a specific URI. They are sent automatically to the server with every request and is persistent for a certain amount of time.
Spring Security implements Authentication via Filter Chain (org.springframework.security.web.SecurityFilterChain) which by default returns 401 UNAUTHORIZED if the Principal is not authenticated.
Authorization
Spring Security provides Authorization via Role-Based Access Control (RBAC), which means Principal has a number of Roles. Each Resource (or operation) specifies which Roles a Principal must have in order to perform actions.
Same Origin Policy
-
Same Origin Policy (SOP) is the most basic mechanism of protection that a server implements. Only scripts which are contained in a web page are allowed to send requests to the origin (URI) of the web page.
-
Cross-Origin Resource Sharing is a way servers can cooperate to relax the SOP, by explicitly allowing a lost of "allowed origins". This is handy if a system consists of services running on machines with different URIs (Microservices). Spring Seciruty provides the
@CrossOriginannotation allowing to specify a lost of allowed sites and allowing by default all origins.
Common Web Exploits
-
Cross-Site Request Forgery (CSRF) happens when a malicious piece of code sends a request to a server where a user is authenticated. CSRF Token is an unique token generated on each request making it harder for an outside actor to insert itself into the conversation.
-
Cross-Site Scripting (XSS) is more dangerous and occurs when an attacker tricks a victim application into executing arbitrary code, ex. saving a string into the DB containing a malicious
<script>tag. Unlike CSRF, XSS attacks don’t depend on Authentication. The attacks can be mitigated by properly escaping the special HTML characters.
Lab
-
Add a Spring Security dependency.
dependencies { ... implementation 'org.springframework.boot:spring-boot-starter-security' ... }Upon adding the dependency, Spring Security is enabled. By default, all endpoints require authentication.
-
If accessed via a browser, unauthenticated requests may redirect to a login page (which might not exist if no view is configured).
-
If accessed via a REST client, a
401 UNAUTHORIZEDresponse is returned. -
Application errors still result in
500 INTERNAL_SERVER_ERROR;403 FORBIDDENonly occurs when the user is authenticated but lacks necessary authorization.
-
-
Add a minimum configuration of Filter Chain
@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); }Upon adding the configuration, all endpoints still require authentication, and unauthenticated requests will return
401 UNAUTHORIZED. No endpoints become accessible yet. -
Configure basic authentication.
@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(request -> request // All HTTP requests to cashcards/ require to be authenticated using HTTP Basic Authentication security (username and password). .requestMatchers("/cashcards/**").authenticated() ) .httpBasic(Customizer.withDefaults()) // Do not require CSRF security. .csrf(csrf -> csrf.disable()); return http.build(); }Upon adjusting the configuration, the rest endpoints return
401 UNAUTHORIZEDinstead as the requests must supply a username and password. -
Add an in-memory user details.
@Bean UserDetailsService testOnlyUsers(PasswordEncoder passwordEncoder) { User.UserBuilder users = User.builder(); UserDetails sarah = users .username("sarah1") .password(passwordEncoder.encode("abc123")) .roles() // No roles for now .build(); return new InMemoryUserDetailsManager(sarah); }Upon adding, the endpoints protected by authentication become accessible using HTTP Basic credentials (e.g.,
sarah1:abc123). -
Adjust the in-memory user details with roles and enable role-based security.
@Bean UserDetailsService testOnlyUsers(PasswordEncoder passwordEncoder) { User.UserBuilder users = User.builder(); UserDetails sarah = users .username("sarah1") .password(passwordEncoder.encode("abc123")) .roles("CARD-OWNER") // new role .build(); UserDetails hankOwnsNoCards = users .username("hank-owns-no-cards") .password(passwordEncoder.encode("qrs456")) .roles("NON-OWNER") // new role .build(); return new InMemoryUserDetailsManager(sarah, hankOwnsNoCards); }@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(request -> request // enable RBAC: Replace the .authenticated() call with the hasRole(...) call. .requestMatchers("/cashcards/**").hasRole("CARD-OWNER")) .httpBasic(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()); return http.build(); }Upon adjusting the configuration, the endpoints are accessible for the users with the correct role and
403 FORBIDDENfor the remaining users. -
Restrict resources ownership.
interface CashCardRepository extends CrudRepository<CashCard, Long>, PagingAndSortingRepository<CashCard, Long> { CashCard findByIdAndOwner(Long id, String owner); Page<CashCard> findByOwner(String owner, PageRequest pageRequest); }@GetMapping("/{requestedId}") private ResponseEntity<CashCard> findById(@PathVariable Long requestedId, Principal principal) { Optional<CashCard> cashCardOptional = Optional.ofNullable(cashCardRepository.findByIdAndOwner(requestedId, principal.getName())); if (cashCardOptional.isPresent()) { return ResponseEntity.ok(cashCardOptional.get()); } else { return ResponseEntity.notFound().build(); } }
Spring Security issued a guidance regarding non-browser clients.
When should you use CSRF protection? Our recommendation is to use CSRF protection for any request that could be processed by a browser by normal users. If you are only creating a service that is used by non-browser clients, you will likely want to disable CSRF protection.
Course: Building a REST API with Spring Boot Module 2: Developing a Secure App Lesson: https://spring.academy/courses/building-a-rest-api-with-spring-boot/lessons/implementing-put
If you need the server to return the Location header of the created resource, then you must use POST.
Alternatively, when the resource URI is known at creation time (for example Invoice API), you can use PUT.
| HTTP Method | Operation | Definition of Resource URI | What does it do? | Response Status Code | Response Body |
|---|---|---|---|---|---|
POST |
Create |
Server generates and returns the URI |
Creates a sub-resource ("under" or "within" the passed URI) |
201 CREATED |
The created resource |
PUT |
Create |
Client supplies the URI |
Creates a resource (at the Request URI) |
201 CREATED |
The created resource |
PUT |
Update |
Client supplies the URI |
Replaces the resource: The entire record is replaced by the object in the Request |
204 NO CONTENT |
(empty) |
PATCH |
Update |
Client supplies the URI |
Partial Update: modify only fields included in the request on the existing record |
200 OK |
The updated resource |
Lesson: Implementing Delete
Delete Options
-
Hard Delete is a simple option and to delete the record from the database. With a hard delete, it’s gone forever.
-
Soft Delete is an alternative which works by marking records as "deleted" in the database, so they are retained, but marked as deleted, ex. via boolean
IS_DELETEDor timestampDELETED_DATE.
Audit Trail and Archiving
If we use Hard Delete it is recommended to store additional data to know when and who deleted a record:
-
Archive the deleted data into a different location.
-
Add audit fields to the record itself, for example
DELETED_DATEorDELETED_BY_USER. This is not limited to Delete operations, but Create and Update also.