Security

djmil 2023-07-24 14:18:28 +02:00
parent 7f29f97969
commit 6e6ca79ae0

431
Security.md Normal file

@ -0,0 +1,431 @@
# Defining our security requirements
Who should be allowed to manage any given Cash Card?
In our simple domain, let's state that the user who _created_ the Cash Card "owns" the Cash Card. Thus, they are the "card owner". Only the card owner can view or update a Cash Card.
The logic will be something like this:
> IF the user is _authenticated_
> ... AND they are _authorized_ as a "card owner"
> ... ... AND they own the requested Cash Card
> THEN complete the users's request
> BUT do not allow users to access Cash Cards they do not own.
Meaning that we'll secure our Family Cash Card API and restrict access to any given Cash Card to the card's "owner".
# The `owner` concept
The `owner` is the unique identity of the person who created and can manage a given Cash Card.
- `owner` added as a field to the `src\main\java\djmil\cashcard\CashCard.java` record
```java
public record CashCard(@Id Long id, Double amount, String owner) {
```
- `owner` added to all `.sql` files in `src/test/resources/`
- `schema.sql`
```sql
CREATE TABLE cash_card
(
ID BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
AMOUNT NUMBER NOT NULL DEFAULT 0,
OWNER VARCHAR(256) NOT NULL
);
```
- `data.sql`
```sql
INSERT INTO CASH_CARD(ID, AMOUNT, OWNER) VALUES (101, 150.00, 'sarah1');
...
```
- `owner` added to all `.json` files in `src/test/resources/example/cashcard`
```json
{
"id": 99,
"amount": 123.45,
"owner": "sarah1"
}
```
- All application code and tests are updated to support the new `owner` field.
```java
@Test
public void cashCardSerializationTest() throws IOException {
CashCard cashCard = new CashCard(99L, 123.45, "sarah1");
```
No functionality has changed as a result of these updates.
# Minimal and non functional Spring Security
`build.gradle`
```yml
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// Add the following dependency
implementation 'org.springframework.boot:spring-boot-starter-security'
...
```
`src/main/java/djmil/cashcard/SecurityConfig.java`
```java
package djmil.cashcard;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.*;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
```
The `@Configuration` annotation tells Spring to use this class to configure Spring and Spring Boot itself. Any Beans specified in this class will now be available to Spring's Auto Configuration engine.
Spring Security expects a Bean to configure its **Filter Chain**. Annotating a method returning a `SecurityFilterChain` with the `@Bean` satisfies this expectation.
```java
@Bean
public SecurityFilterChain filterChain
```
At this moment all tests should pass _except for the test for creating a new `CashCard` via a `POST`_.
```shell
CashCardApplicationTests > shouldCreateANewCashCard() FAILED
org.opentest4j.AssertionFailedError:
expected: 201 CREATED
but was: 403 FORBIDDEN
...
11 tests completed, 1 failed
```
# Basic Authentication
Update `SecurityConfig.filterChain` with the following to enable basic authentication:
```java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.requestMatchers("/cashcards/**")
.authenticated()
.and()
.csrf().disable()
.httpBasic();
return http.build();
}
```
Here if we explain Spring Security's builder pattern in more understandable language, we see:
> All HTTP requests to `cashcards/` endpoints are required to be authenticated using HTTP Basic Authentication security (username and password).
>
> Also, do not require CSRF security.
All application tests should fail with a `401 UNAUTHORIZED` HTTP status code. Thus, it is time to add some authentication code!
## Create a test user
In `SecurityConfig`:
```java
@Bean
public 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);
}
```
This `UserDetailsService` configuration should be understandable: configure a user named `sarah1` with password `abc123`. The Spring's IoC container will find the `UserDetailsService` Bean and Spring Data will use it when needed.
## Add basic Auth in HTTP tests
Each HTTP request shall be updated with basic authentication for `sarah1`:
```java
void shouldReturnACashCardWhenDataIsSaved() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("sarah1", "abc123") // Basic authentication
.getForEntity("/cashcards/99", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
...
```
## Testing for incorrect credentials
```java
@Test
void shouldNotReturnACashCardWhenUsingBadCredentials() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("BAD-USER", "abc123")
.getForEntity("/cashcards/99", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
response = restTemplate
.withBasicAuth("sarah1", "BAD-PASSWORD")
.getForEntity("/cashcards/99", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
```
# Authorization
It is likely that a user service will provide access to many authenticated users, but only "card owners" should be allowed to access Family Cash Cards managed by our application. Here we'll implement Role-Based Access Control (RBAC).
## Assign roles to the User
To test authorization, we need multiple test users with a variety of roles. Update `SecurityConfig.testOnlyUsers` and add the `CARD-OWNER` role to `sarah1`. Also, let's add a new user named "hank-owns-no-cards" with a role of `NON-OWNER`.
```java
...
@Bean
public 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);
}
```
## Role verification
```java
@Test
void shouldRejectUsersWhoAreNotCardOwners() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("hank-owns-no-cards", "qrs456")
.getForEntity("/cashcards/99", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
}
```
We see that our new test fails when we run it.
```shell
CashCardApplicationTests > shouldRejectUsersWhoAreNotCardOwners() FAILED
org.opentest4j.AssertionFailedError:
expected: 403 FORBIDDEN
but was: 200 OK
```
Why was `hank-owns-no-cards` able to access a `CashCard` as indicated by the `200 OK` response?
Although we have given the test users roles, we are _not enforcing_ role-based security.
## Role enforcing
In `SecurityConfig.filterChain` we can restrict access to only users with the CARD-OWNER role. Previously, the complete access was granted to **all** authenticated users! Now, we will grant complete access to users with `CARD-OWNER` role only. Which is better than it was before, but less than ideal. Since any authenticated user with role `CARD-OWNER` can view anyone else's Family Cash Cards!
```java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.requestMatchers("/cashcards/**")
//.authenticated()
.hasRole("CARD-OWNER") // <<-- enable RBAC
.and()
.csrf().disable()
.httpBasic();
return http.build();
}
```
# Roles and Ownership
As mentioned in the [[Security#Role enforcing]] paragraph, we have a glaring security hole in our application! The security technologies such as Spring Security are amazing. There are features such as _Spring Security Method Security_ that might help in this situation, but it is still _your responsibility as a developer_ to write secure code and follow best security practices:
> For example, don't write code that allows users to access other users' data!
To fix this, we will update our tests, `CashCardRepository`, and `CashCardController`.
## Synopsis
To better understand what is going on and how to fix this, do the following:
- Add a new `CashCard` for a user named `kumar2`
```sql
...
INSERT INTO CASH_CARD(ID, AMOUNT, OWNER) VALUES (102, 200.00, 'kumar2');
```
- Test that users cannot access each other's data
```java
@Test
void shouldNotAllowAccessToCashCardsTheyDoNotOwn() {
ResponseEntity<String> response = restTemplate
.withBasicAuth("sarah1", "abc123")
.getForEntity("/cashcards/102", String.class); // <<-- kumar2's data
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
```
So now we have a test, where `sarah1` attempts to access `kumar2`'s data.
>**_Note:_** You might wonder why we want to return a `404 NOT FOUND` response instead of something else, like `401 UNAUTHORIZED`. One argument in favor of choosing to return `NOT FOUND` is that it's the same response that we'd return if the requested Cash Card doesn't exist. It's safer to err on the side of not revealing **any** information about data which is not authorized for the user.
Currently, user `sarah1` is able to view `kumar2`'s data because:
- `sarah1` is _authenticated_.
- `sarah1` is an _authorized_ `CARD-OWNER`. As expected, our new test fails, along with many others.
```shell
CashCardApplicationTests > shouldNotAllowAccessToCashCardsTheyDoNotOwn() FAILED
org.opentest4j.AssertionFailedError:
expected: 404 NOT_FOUND
but was: 200 OK
```
In addition, our test for fetching a list of CashCards is also failing:
```shell
CashCardApplicationTests > shouldReturnAllCashCardsWhenListIsRequested() FAILED
org.opentest4j.AssertionFailedError:
expected: 3
but was: 4
```
## Restrict `CashCardRepository` queries "scope" to the correct `OWNER`
The simplest thing we can do is to always filter our data access by `CashCard` `owner`. We will need to filter by `owner` when finding both a single `CashCard` or a list of `CashCards`.
Edit `CashCardRepository` to add a new finder methods.
```java
...
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
public interface
CashCardRepository
extends
CrudRepository<CashCard, Long>,
PagingAndSortingRepository<CashCard, Long>
{
CashCard findByIdAndOwner(Long id, String owner); // new method
Page<CashCard> findByOwner(String owner, PageRequest amount); // new method
}
```
We need only to define the methods, since Spring Data is perfectly capable of generating the actual SQL for the queries we need. Thanks, Spring Data!
> **_Note:_** You might wonder whether Spring Data allows you to write your own SQL. After all, Spring Data can't anticipate every need, right? The answer is Yes! It's easy for you to write your own SQL code. The [Spring Data Query Methods](https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#jdbc.query-methods) documentation describes how to do so by using the `@Query` annotation.
## The Principal
The `CashCardRepository` now supports filtering `CashCard` data by `owner`. But we're not using this new functionality. Let's enhance our app security by introducing a concept the [Principal](https://stackoverflow.com/questions/37499307/whats-the-principal-in-spring-security) concept to the `CashCardController`. As with other helpful objects, the `Principal` is available for us to use in our Controller. The `Principal` holds our user's authenticated, authorized information.
```java
import java.security.Principal;
...
@GetMapping("/{requestedId}")
public ResponseEntity<CashCard> findById(@PathVariable Long requestedId, Principal principal) {
Optional<CashCard> cashCardOptional = Optional.ofNullable(cashCardRepository.findByIdAndOwner(
requestedId,
principal.getName()));
if (cashCardOptional.isPresent()) {
...
@GetMapping
public ResponseEntity<List<CashCard>> findAll(Pageable pageable, Principal principal) {
Page<CashCard> page = cashCardRepository.findByOwner(
principal.getName(),
PageRequest.of(
pageable.getPageNumber(),
...
```
Note that `Principal.name()` will return the username provided from Basic Auth.
## Creation
We have one more remaining security hole: creating `CashCards`. The authenticated, authorized Principal should be used as the `owner` when creating a new `CashCard`.
> *Question*: What would happen if we automatically used the submitted `owner` value?
> *Answer*: We risk allowing users to create `CashCards` for _someone else_!
Let's ensure that only the authenticated, authorized Principal owns the `CashCards` they are creating. To prove that we do not need to submit an `owner`, thus `null` shall be used as the `owner` for the `CashCard`.
```java
@Test
void shouldCreateANewCashCard() {
CashCard newCashCard = new CashCard(null, 250.00, null);
...
```
What do you think will happen when we run the updated test?
```shell
CashCardApplicationTests > shouldCreateANewCashCard() FAILED
org.opentest4j.AssertionFailedError:
expected: 201 CREATED
but was: 500 INTERNAL_SERVER_ERROR
```
Our application is crashing due to the missing `owner` which is required by the database during the creation of the new CashCard entry. Review `test/resources/schema.sql` to see more.
```sql
CREATE TABLE cash_card
(
...
OWNER VARCHAR(256) NOT NULL
);
```
To fix this, we have to update `POST` endpoint in the Controller. Once again we will use the provided `Principal` to ensure that the correct `owner` is saved with the new `CashCard`.
```java
@PostMapping
private ResponseEntity<Void> createCashCard(@RequestBody CashCard newCashCardRequest, UriComponentsBuilder ucb, Principal principal) {
CashCard cashCardWithOwner = new CashCard(null, newCashCardRequest.amount(), principal.getName());
CashCard savedCashCard = cashCardRepository.save(cashCardWithOwner);
...
```
Here, instead of immediately saving `newCashCardRequest` parsed from request body, we are extracting major information from it. The information than used to create another `cashCardWithOwner`, with has owner set to the value provided from `Principal`.
# Dealing with Cross-Site Request Forgery (CSRF)
As we learned in the accompanying lesson, protection against Cross-Site Request Forgery (CSRF, or "sea-surf") is an important aspect of HTTP-based APIs used by web-based applications. Yet, we've disabled CSRF via the `.csrf().disable()` line in `SecurityConfig.filterChain`.
Why have we disabled CSRF?
For the purposes of our Family Cash Card API, we're going to follow the guidance from the Spring Security team [regarding non-browser clients](https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html/csrf.html#when-to-use-csrf-protection):
> 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.
Meaning, that our service is a Backend server that is most likely to be used by our user facing Frontend server, thus it is safe to disable CSRF protection in such a scenario.
If you would like to add CSRF security to Front-facing applications, please review the testing support options below.
- [MockMVC CSRF testing examples](https://docs.spring.io/spring-security/reference/servlet/test/mockmvc/csrf.html)
- [WebTestClient CSRF testing examples](https://docs.spring.io/spring-security/site/docs/5.2.0.RELEASE/reference/html/test-webflux.html#csrf-support).
- A description of the [Double-Submit Cookie Pattern](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie).
- The Cash Card codebase, with CSRF protection enabled and implementing tests using the Double-Submit Cookie pattern: [TestRestReplate CSRF testing examples](https://github.com/vmware-tanzu-learning/course-spring-brasb-build-a-rest-api-code/blob/simple-security-csrf-testing/src/test/java/example/cashcard/CashCardApplicationTests.java).