Now it is time to start customizing the auto-configuration.
As soon as you customize any bit of Spring Security the complete spring boot auto-configuration will back-off.
Custom authentication with persistent users
Before we start let's look into some internal details how spring security works for the servlet web stack.
By using a ServletFilter you can add functionality that is called around each request and response. Spring Security provides several web filter out of the box.
Filter
Description
AuthenticationWebFilter
Performs authentication of a particular request
AuthorizationWebFilter
Determines if an authenticated user has access to a specific object
CorsWebFilter
Handles CORS preflight requests and intercepts
CsrfWebFilter
Applies CSRF protection using a synchronizer token pattern.
Spring Security WebFilter
Security Filter Chain
Spring Security configures security by utilizing a Security Filter Chain
In step 1 we just used the auto configuration of Spring Boot. This configured a default security filter chain.
As part of this lab we will customize several things for authentication:
Connect the existing persistent user data with Spring Security to enable authentication based on these
Encode the password values to secure hashed ones in the database
Ensure a password policy to enforce secure passwords (a common source of hacking authentication are weak passwords)
Encrypting Passwords
We start by replacing the default user/password with our own persistent user storage (already present in DB). To do this we add a new class WebSecurityConfiguration to package com.example.libraryserver.config having the following contents.
Configures a PasswordEncoder. A password encoder is used by spring security to encrypt passwords and to check if a given password matches the encrypted one.
You may recognize as well a legacy password encoder (this will be used later in this lab for password upgrades)
Validates the given cleartext password with the encrypted one (without revealing the unencrypted one)
In spring security 5 creating an instance of the DelegatingPasswordEncoder is much easier by using the class PasswordEncoderFactories. In past years several previously used password encryption algorithms have been broken (like MD4 or MD5). By using PasswordEncoderFactories you always get a configured PasswordEncoder that uses an PasswordEncoder with a state of the art encryption algorithm like BCrypt or Argon2 at the time of creating this workshop.
Now that we already have configured the encrypting part for passwords of our user storage we need to connect our own user store (the users already stored in the DB) with spring security's authentication manager.
This is done in two steps:
In the first step we need to implement spring security's definition of a user implementing UserDetails. Please create a new class called AuthenticatedUser in package com.example.libraryserver.security.
To make it a bit easier we just extend our existing User data class.
In the second step we need to implement spring security's interface UserDetailsService to integrate our user store with the authentication manager. Please go ahead and create a new class LibraryUserDetailsService in package com.example.libraryserver.security:
packagecom.example.libraryserver.security;importcom.example.libraryserver.user.service.UserService;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsPasswordService;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.stereotype.Service;@Qualifier("library-user-details-service")@ServicepublicclassLibraryUserDetailsServiceimplementsUserDetailsService,UserDetailsPasswordService {privatestaticfinalLogger LOGGER =LoggerFactory.getLogger(LibraryUserDetailsService.class);privatefinalUserService userService;publicLibraryUserDetailsService(UserService userService) {this.userService= userService; } @OverridepublicUserDetailsloadUserByUsername(String username) throwsUsernameNotFoundException {return userService.findOneByEmail(username).map(AuthenticatedUser::new).orElseThrow(() ->newUsernameNotFoundException("No user found for "+ username)); } @OverridepublicUserDetailsupdatePassword(UserDetails user,String newPassword) {return userService.findOneByEmail(user.getUsername()).map( u -> {LOGGER.info("Upgrading password {} for user {} to {}",user.getPassword(),user.getUsername(), newPassword);u.setPassword(newPassword);returnnewAuthenticatedUser(userService.save(u)); }).orElseThrow( () ->newUsernameNotFoundException("No user found for "+user.getUsername())); }}
LibraryUserDetailsService.java
After completing this part of the workshop we now still have the auto-configured SecurityWebFilterChain but we have replaced the default user with our own users from our DB persistent storage.
If you restart the application now you have to use the following user credentials to log in:
Username
Email
Password
Role
bwayne
bruce.wayne@example.com
wayne
LIBRARY_USER
bbanner
bruce.banner@example.com
banner
LIBRARY_USER
pparker
peter.parker@example.com
parker
LIBRARY_CURATOR
ckent
clark.kent@example.com
kent
LIBRARY_ADMIN
Authenticated Principal
As we now have a persistent authenticated user we can now also use this user to check if the current user is allowed to borrow or return a book. This requires changes in BookService and BookRestController.
First change the class BookService:
packagecom.example.libraryserver.book.service;importcom.example.libraryserver.book.data.Book;importcom.example.libraryserver.book.data.BookRepository;importcom.example.libraryserver.security.AuthenticatedUser;importcom.example.libraryserver.user.data.UserRepository;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importorg.springframework.util.IdGenerator;importjava.util.List;importjava.util.Optional;importjava.util.UUID;@Service@Transactional(readOnly =true)publicclassBookService {privatestaticfinalLogger LOGGER =LoggerFactory.getLogger(BookService.class);privatefinalBookRepository bookRepository;privatefinalUserRepository userRepository;privatefinalIdGenerator idGenerator;publicBookService(BookRepository bookRepository,UserRepository userRepository,IdGenerator idGenerator) {this.bookRepository= bookRepository;this.userRepository= userRepository;this.idGenerator= idGenerator; }// ... @TransactionalpublicOptional<Book> borrowForUser(UUID bookIdentifier,UUID userIdentifier,AuthenticatedUser authenticatedUser) {LOGGER.trace("borrow book with identifier {} for user with identifier {}", bookIdentifier, userIdentifier);return bookRepository.findOneByIdentifier(bookIdentifier).filter( b ->b.getBorrowedByUser() ==null&& authenticatedUser !=null&&userIdentifier.equals(authenticatedUser.getIdentifier())).flatMap( b -> userRepository.findOneByIdentifier(userIdentifier).map( u -> {b.setBorrowedByUser(u);Book borrowedBook =bookRepository.save(b);LOGGER.info("Borrowed book {} for user {}", borrowedBook, u);returnOptional.of(borrowedBook); }).orElse(Optional.empty())); } @TransactionalpublicOptional<Book> returnForUser(UUID bookIdentifier,UUID userIdentifier,AuthenticatedUser authenticatedUser) {LOGGER.trace("return book with identifier {} of user with identifier {}", bookIdentifier, userIdentifier);return bookRepository.findOneByIdentifier(bookIdentifier).filter( b ->b.getBorrowedByUser() !=null&& authenticatedUser !=null&&b.getBorrowedByUser().getIdentifier().equals(userIdentifier)&&b.getBorrowedByUser().getIdentifier().equals(authenticatedUser.getIdentifier())).flatMap( b -> userRepository.findOneByIdentifier(userIdentifier).map( u -> {b.setBorrowedByUser(null);Book returnedBook =bookRepository.save(b);LOGGER.info("Returned book {} for user {}", returnedBook, u);returnOptional.of(returnedBook); }).orElse(Optional.empty())); }//...}
BookService.java
Then please adapt the BookRestController accordingly.
We already looked into the DelegatingPasswordEncoder and PasswordEncoderFactories. As these classes have knowledge about all encryption algorithms that are supported in spring security, the framework can detect an outdated encryption algorithm. If you look at the LibraryUserDetailsService class we just have added this also implements the additionally provided interface UserDetailsPasswordService. This way we can now enable an automatic password encryption upgrade mechanism.
The UserDetailsPasswordService interface just defines one more operation.
We already have a user using a password that is encrypted using an outdatedMD5 algorithm. We achieved this by defining a legacy user doctor.strange@example.com with password strange in the existing DataInitializer class.
Now restart the application and see what happens if we try to get the list of books using this new user (username='doctor.strange@example.com', password='strange').
In the console you should see the log output showing the old MD5 password being updated to bcrypt password.
CAUTION: Never log any sensitive data like passwords, tokens etc., even in encrypted format. Also never put such sensitive data into your version control. And never let error details reach the client (via REST API or web application). Make sure you disable stacktraces in client error messages using property server.error.include-stacktrace=never
Actuator Security
It is also a good idea to restrict the details of the health actuator endpoint to authenticated users. Anonymous users should only see the UP or DOWN status values but no further details.
Usually not the authentication mechanisms are hacked, instead weak passwords are the most critical source for attacking authentication. Therefore strong passwords are really important. It is also not a good practice any more to force users to change their passwords after a period of time.
We also check for well-known insecure passwords using a password list. There are plenty of password lists available on the internet (especially useful for performing brute force attacks by hackers). Please download one of these the password list from https://github.com/danielmiessler/SecLists/blob/master/Passwords/darkweb2017-top100.txt and store this file as password-list.txt in folder src/main/resources.
We also need a special error for reporting password policy violations. Please create a new class InvalidPasswordError.