workshop-tutorial
workshop-tutorial
Workshop
Andreas Falk
Version 1.0.0-SNAPSHOT
Table of Contents
1. Requirements for this workshop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2. Common Web Security Risks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
3. Reactive Systems & Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
3.1. Project Reactor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
3.2. Spring WebFlux. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4. Intro-Lab: Reactive Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
5. The workshop application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
6. Basic Security Labs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
6.1. Run the initial application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
6.2. Lab 1: Auto Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
6.2.1. Login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
6.2.2. Common Security Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
6.2.3. Logout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
6.3. Lab 2: Customize Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
6.3.1. WebFilter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
6.3.2. WebFilterChainProxy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
6.3.3. Step 1: Encoding Passwords . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
6.3.4. Step 2: Persistent User Storage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
6.4. Automatic Password Encoding Updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
6.5. Lab 3: Add Authorization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
6.6. Lab 4: Security Testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
7. OAuth2/OpenID Connect Labs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
7.1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
7.1.1. OAuth 2.0. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
7.1.2. OpenID Connect 1.0 (OIDC). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
7.1.3. Tokens in OIDC and OAuth 2.0. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
7.1.4. OAuth2/OIDC in Spring Security 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
7.1.5. What we will build . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
7.2. Setup: Keycloak Identity Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
7.3. Intro-Lab: Authorization Code Demo Client. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
7.4. Lab 5: OpenID Connect Resource Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
7.4.1. Gradle dependencies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
7.4.2. Implementation steps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
7.5. Lab 6: OpenID Connect Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
7.5.1. Gradle dependencies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
7.5.2. Implementation steps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
8. Feedback. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Appendix A: Copyright and License. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Figure 1. Reactive Spring Security 5 Workshop
— Jim Manico
You will make your hands dirty in code in the following steps:
1. Add spring boot security starter dependency for simple auto configuration of security
5. Experiment with new OAuth2 Login Client and Resource Server of Spring Security 5
1
Chapter 1. Requirements for this workshop
• Git
• Any Java IDE capable of building with Gradle (IntelliJ, Eclipse, VS Code, …)
• Basic knowledge of Reactive Systems and reactive programming using Spring WebFlux &
Reactor
As we are building the samples using Gradle your Java IDE should be capable use this. As IntelliJ
user support for Gradle is included by default. As an Eclipse user you have to install a plugin via the
marketplace
To get the workshop project you either can just clone the repository using
2
or
After that you can import the workshop project into your IDE
3
Chapter 2. Common Web Security Risks
In this workshop you will strive various parts of securing a web application that fit into the OWASP
Top 10 2017 list.
The Open Web Application Security Project has plenty of further free resources available.
As a developer you may also have a look into the OWASP ProActive Controls document which
describes how to develop your applications using good security patterns. To help getting all security
requirements right for your project and how to test these the Application Security Verification
Standard can help you here.
You will find more sources of information about security referenced in the
References section.
4
Chapter 3. Reactive Systems & Streams
The following subsections give a very condensed introduction to the basics of Reactive Systems, the
Project Reactor and Spring WebFlux.
This might help to better understand the sample application for beginners of Reactive.
— https://www.reactivemanifesto.org
• Responsiveness means the system responds in a timely manner and is the cornerstone of
usability
• Elasticity means that the throughput of a system scales up or down automatically to meet
varying demand as resource is proportionally added or removed
— http://www.reactive-streams.org/
• Back-Pressure: When one component is struggling to keep-up, the system as a whole needs to
respond in a sensible way. Back-pressure is an important feedback mechanism that allows
systems to gracefully respond to load rather than collapse under it.
Reactor is a fully non-blocking foundation and offers backpressure-ready network engines for
HTTP (including Websockets), TCP and UDP.
Reactor introduces composable reactive types that implement Publisher but also provide a rich
vocabulary of operators, most notably Flux and Mono.
A Mono<T> is a specialized Publisher<T> that emits at most one item and then optionally terminates
with an onComplete signal or an onError signal.
5
Figure 4. Mono, an Asynchronous 0-1 Result (source: projectreactor.io)
Spring Webflux depends on Reactor and uses it internally to compose asynchronous logic and to
provide Reactive Streams support. It provides two programming models:
• Annotated Controllers: This is uses the same annotations as in the Spring MVC part
6
Chapter 4. Intro-Lab: Reactive Programming
Before we dive into the world of security, you have the chance to get a first glimpse on the
difference between imperative and reactive programming style.
The following resources might be helpful for first steps into the reactive world:
7
Chapter 5. The workshop application
In this workshop you will be provided a finished but completely unsecured reactive web
application. This library server application provides a RESTful service for administering books and
users.
You can find this provided workshop application in sub project lab-1/initial-library-server.
This will also be your starting point into the hands-on part that we will dive into shortly.
The RESTful service for books is build using the Spring WebFlux annotation model and the RESTful
service for users is build using the Spring WebFlux router model.
The application contains a complete documentation for the RESTful API build with spring rest docs
which you can find in the directory build/asciidoc/html5 after performing a full gradle build.
The domain model of this application is quite simple and just consists of Book and User. The
packages of the application are organized as follows:
• business: All the service classes (quite simple for workshop, usually containing business logic)
8
To call the provided REST API you can use curl or httpie. For details on how to call
the REST API please consult the REST API documentation which also provides
sample requests for curl and httpie.
• Standard users: A standard user can borrow and return his currently borrowed books
If you are going into reactive systems this works best if all layers work in non-blocking reactive
style. Therefore the application is build using:
9
Chapter 6. Basic Security Labs
To start the workshop please begin by adapting the lab-1/initial-library-server
If you are not able to keep up with completing a particular step you always can
just start over with the existing application of next step.
For example if you could not complete the lab 1 in time just continue with lab 2
using lab-1/complete-library-server as new starting point.
This should also start the embedded MongoDB instance. In case you get an error here telling that
the corresponding port 40495 is already bound to another service then please change the
configuration to a different port in file application.yml:
spring:
data:
mongodb:
port: 40495
To look into the embedded MongoDB instance the UI tool Robo 3T is very helpful and easy to use. In
case you did not yet install this tool just go to https://robomongo.org/download and download the
corresponding file for your operating system (please do NOT download the commercial variant
named Studio 3T).
After you downloaded the install file and extracted/installed the tool you have to configure the
connection to the embedded MongoDB like in the following figure. If you have configured your
embedded MongoDB instance to a different port then use that one instead of 40495.
10
Figure 8. Robo 3T Connection configuration
Finally you can navigate your web browser to http://localhost:9091/books then you should see a list
of books.
You can achieve the same on the command line using httpie or curl.
Curl
curl 'http://localhost:9091/books' | jq
Httpie
http localhost:9091/books
If both the workshop application and the Robo 3T tool run fine and you could see the list of books
then you are setup and ready to start the first lab.
build.gradle
dependencies {
...
implementation('org.springframework.boot:spring-boot-starter-security') ①
...
testImplementation('org.springframework.security:spring-security-test') ②
}
11
② Adds testing support for spring security
6.2.1. Login
Spring Security 5 added a nicer auto-generated login form (build with bootstrap library).
If you browse to localhost:8080/books then you will notice that a login form appears in the browser
window.
But wait - what are the credentials for a user to log in?
With spring security autoconfigured by spring boot the credentials are as follows:
• Username=user
console log
Of course you won’t use the generated password for any serious application as this will change with
each restart of the application.
Instead you can easily just change the password to a static value by changing the application.yaml
file.
12
application.yml
spring:
...
security:
user:
password: secret
Please make sure the indents of the content is correct in yaml formatted files. If
this is not the case you can get really strange errors sometimes.
As you can see, if Spring Security is on the classpath, then the web application is secured by default.
Spring boot auto-configured basic authentication and form based authentication for all web
endpoints.
This also applies to all actuator endpoints like /actuator/health. All monitoring web endpoints can
now only be accessed with an authenticated user. See Actuator Security for details.
Additionally spring security improved the security of the web application automatically for:
• Session Fixation: Session Fixation is an attack that permits an attacker to hijack a valid user
session.
If you want to learn more about this please read the Session Fixation page at OWASP
• Cross Site Request Forgery (CSRF): Cross-Site Request Forgery (CSRF) is an attack that forces an
end user to execute unwanted actions on a web application in which they’re currently
authenticated.
If you want to know what CSRF really is and how to mitigate this attack please consult CSRF
attack description at OWASP
• Default Security Headers: This automatically adds all recommended security response headers
to all http responses. You can find more information about this topic in the OWASP Secure
Headers Project
13
default security response headers
6.2.3. Logout
Spring security 5 also added a bit more user friendly logout functionality out of the box. If you
direct your browser to localhost:8080/logout you will see the following dialog on the screen.
The application already contains a central error handler for the whole web application using
@RestControllerAdvice. You can find this in class com.example.library.server.api.ErrorHandler.
To handle potential future access denied error we add the following new block to this class:
com.example.library.server.api.ErrorHandler
import org.springframework.security.access.AccessDeniedException;
@RestControllerAdvice
public class ErrorHandler {
@ExceptionHandler(AccessDeniedException.class)
public Mono<ResponseEntity<String>> handle(AccessDeniedException ex) {
Logger logger = LoggerFactory.getLogger(this.getClass());
logger.error(ex.getMessage());
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).build());
}
...
Now all requests in the tests require an authenticated user and fail with http status 401
(Unauhorized).
14
All tests using requests with POST, PUT or DELETE methods are failing with http status 403
(Forbidden). These requests are now protected against CSRF attacks and require CSRF tokens.
To achieve authentication in the tests you have to add the annotation @WithMockUser on class
level. CSRF is handled by mutating the requests inside the tests by adding this snippet to the failing
tests:
com.example.library.server.api.BookApiDocumentationTest
...
import static
org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.cs
rf;
...
webTestClient
.mutateWith(csrf())
...
Now let’s proceed to next step and start with customizing the authentication part.
Before we start let’s look into some internal details how spring security works for the reactive web
stack.
6.3.1. WebFilter
Like the javax.servlet.Filter in the blocking servlet-based web stack there is a comparable
functionality in the reactive world: The WebFilter.
15
WebFilter
/**
* Process the Web request and (optionally) delegate to the next
* {@code WebFilter} through the given {@link WebFilterChain}.
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain);
By using the WebFilter you can add functionality that called around each request and response.
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.
16
LoggingWebFilter
package com.example.library.server.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@Component
public class LoggingWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
LOGGER.info("Request {} called", exchange.getRequest().getPath().value());
return chain.filter(exchange);
}
}
6.3.2. WebFilterChainProxy
In lab 1 we just used the auto configuration of Spring Boot. This configured the default security
settings as follows:
17
Default security configuration (with Spring Boot)
@Configuration
class WebFluxSecurityConfiguration {
...
/**
* The default {@link ServerHttpSecurity} configuration.
* @param http
* @return
*/
private SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
{
http
.authorizeExchange()
.anyExchange().authenticated();
...
}
18
SecurityWebFilterChain
/**
* Defines a filter chain which is capable of being matched against a {@link
ServerWebExchange} in order to decide
* whether it applies to that request.
*
* @author Rob Winch
* @since 5.0
*/
public interface SecurityWebFilterChain {
/**
* Determines if this {@link SecurityWebFilterChain} matches the provided {@link
ServerWebExchange}
* @param exchange the {@link ServerWebExchange}
* @return true if it matches, else false
*/
Mono<Boolean> matches(ServerWebExchange exchange);
/**
* The {@link WebFilter} to use
* @return
*/
Flux<WebFilter> getWebFilters();
}
To customize the spring security configuration you have to implement one or more of
SecurityWebFilterChain configuration methods.
19
WebFilterChainProxy
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { ①
return Flux.fromIterable(this.filters)
.filterWhen( securityWebFilterChain ->
securityWebFilterChain.matches(exchange))
.next()
.switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
.flatMap( securityWebFilterChain ->
securityWebFilterChain.getWebFilters()
.collectList()
)
.map( filters -> new FilteringWebHandler(webHandler ->
chain.filter(webHandler), filters))
.map( handler -> new DefaultWebFilterChain(handler) )
.flatMap( securedChain -> securedChain.filter(exchange));
}
}
① Central point for spring security to step into reactive web requests
We start by replacing the default user/password with our own persistent user storage (already
present in MongoDB). To do this we add a new class WebSecurityConfiguration to package
com.example.library.server.config having the following contents.
20
WebSecurityConfiguration class
package com.example.library.server.config;
import org.springframework.context.annotation.Bean;
import
org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebFluxSecurity ①
public class WebSecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); ②
}
② This adds the new delegating password encoder introduced in spring security 5
1. This adds the SecurityWebFilterChain. If you already have secured servlet based spring mvc web
applications then you might know what’s called the spring security filter chain. In spring
webflux the SecurityWebFilterChain is the similar approach based on matching a request with
one or more WebFilter.
PasswordEncoder interface
package org.springframework.security.crypto.password;
② Validates the given cleartext password with the encrypted one (without revealing the
unencrypted one)
21
algorithms have been broken (like MD4 or MD5). By using PasswordEncoderFactories you always
get a configured DelegatingPasswordEncoder instance that configures a map of PasswordEncoder
instances for the recommended password hashing algorithms like
• Bcrypt
• Scrypt
• PBKDF2
At the time of creating this workshop the DelegatingPasswordEncoder instance configures the
Bcrypt algorithm as the default to be used for encoding new passwords.
If you want to know more about why to use hashing algorithms like Bcrypt, Scrypt or PBKDF2
instead of other ones like SHA-2 then read the very informative blog post About Secure Password
Hashing.
DelegatingPasswordEncoder class
package org.springframework.security.crypto.factory;
② Suitable encoders for decrypting are selected based on prefix in encrypted value
To have encrypted passwords in our MongoDB store we need to tweak our existing DataInitializer a
bit with the PasswordEncoder we just have configured.
22
DataInitializer class
package com.example.library.server;
...
import org.springframework.security.crypto.password.PasswordEncoder;
...
@Component
public class DataInitializer implements CommandLineRunner {
...
private final PasswordEncoder passwordEncoder; ①
@Autowired
public DataInitializer(BookRepository bookRepository, UserRepository
userRepository,
IdGenerator idGenerator, PasswordEncoder passwordEncoder)
{
...
this.passwordEncoder = passwordEncoder;
}
...
Now that we already have configured the encoding part for passwords of our user storage we need
to connect our own user store (the users already stored in the MongoDB) with spring security’s
authentication manager.
23
This is done in two steps:
In the first step we need to implement spring security’s definition of a user called UserDetails.
24
LibraryUser class
package com.example.library.server.security;
import com.example.library.server.dataaccess.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.stream.Collectors;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.commaSeparatedStringToAuthorityList(
getRoles().stream().map(rn -> "ROLE_" +
rn.name()).collect(Collectors.joining(",")));
}
@Override
public String getUsername() {
return getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
25
① To provide our own user store we have to implement the spring security’s predefined interface
UserDetails
LibraryReactiveUserDetailsService class
package com.example.library.server.security;
import com.example.library.server.business.UserService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
public class LibraryReactiveUserDetailsService implements ReactiveUserDetailsService {
①
@Override
public Mono<UserDetails> findByUsername(String username) { ③
return userService.findOneByEmail(username).map(LibraryUser::new);
}
}
① To provide our own user store we have to implement the spring security’s predefined interface
ReactiveUserDetailsService which is the binding component between the authentication service
and our LibraryUser
② To search and load the targeted user for authentication we use our existing UserService
③ This will be called when authentication happens to get user details for validating the password
and adding this user to the security context
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
MongoDB persistent storage.
If you restart the application now you have to use the following user credentials to log in:
26
• LIBRARY_USER: Standard library user who can list, borrow and return his currently borrowed
books
Important: We will use the following users in all subsequent labs from now on:
27
ReactiveUserDetailsPasswordService interface
package org.springframework.security.core.userdetails;
import reactor.core.publisher.Mono;
/**
* Modify the specified user's password. This should change the user's password in
the
* persistent user repository (datbase, LDAP etc).
*
* @param user the user to modify the password for
* @param newPassword the password to change to
* @return the updated UserDetails with the new password
*/
Mono<UserDetails> updatePassword(UserDetails user, String newPassword);
}
First we need a user having a password that is encoded using an outdated hashing algorithm. We
achieve this by modifying the existing DataInitializer class.
28
DataInitializer class
package com.example.library.server;
...
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.MessageDigestPasswordEncoder;
...
...
...
DelegatingPasswordEncoder oldPasswordEncoder =
new DelegatingPasswordEncoder(
"MD5", Collections.singletonMap("MD5", new
MessageDigestPasswordEncoder("MD5"))); ②
...
}
29
① We need an additional user with a password using an old encryption.
② To encrypt a user with an outdated password we have to add an additional password encoder
for MD5 encryption. Never do such a thing in production. Always use the default
PasswordEncoderFactories class instead
③ Here we add another user with password encrypted by added MD5 password encoder
To activate support for automatic password encoding upgrades we need to extend our existing
LibraryReactiveUserDetailsService class.
LibraryReactiveUserDetailsService class
package com.example.library.server.security;
import com.example.library.server.business.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import
org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
public class LibraryReactiveUserDetailsService implements ReactiveUserDetailsService,
ReactiveUserDetailsPasswordService { ①
...
@Override
public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) { ③
return userService.findOneByEmail(user.getUsername())
.doOnSuccess(u -> u.setPassword(newPassword))
.flatMap(userService::update)
.map(LibraryUser::new);
}
}
① To provide our own user store we have to implement the spring security’s predefined interface
ReactiveUserDetailsService which is the binding component between the authentication service
30
and our LibraryUser. Now we also add ReactiveUserDetailsPasswordService to enable automatic
password encryption upgrades
② To log the password upgrade action here we provide a logger. Please note: NEVER log passwords
in production!!
Now restart the application and see what happens if we try to get the list of books using this new
user (username='[email protected]', password='user').
In the console you should see the log output showing the old MD5 password being updated to
Bcrypt password.
Never log any sensitive data like passwords, tokens etc., even in hashed 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
In the next workshop part we also adapt the SecurityWebFilterChain to our needs and add
authorization rules (in web and method layer) for our application.
We know who is using our application (authentication) but we do not have control over what this
user is allowed to do in our application (authorization).
31
OWASP Top 10-2017 A5-Broken Access Control
As a best practice the authorization should always be implemented on different layers like the web
and method layer. This way the authorization still prohibits access even if a user manages to bypass
the web url based authorization filter by playing around with manipulated URL’s.
All the web layer authorization rules are configured in the WebSecurityConfiguration class by
adding a new bean for SecurityWebFilterChain. Here we also already switch on the support for
method layer authorization by adding the annotation @EnableReactiveMethodSecurity.
WebSecurityConfiguration class
package com.example.library.server.config;
...
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity ①
public class WebSecurityConfiguration {
@Bean ②
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.matchers(PathRequest.toStaticResources().atCommonLocations())
.permitAll() ③
32
.matchers(EndpointRequest.to("health"))
.permitAll() ④
.matchers(EndpointRequest.to("info"))
.permitAll()
.matchers(EndpointRequest.toAnyEndpoint())
.hasRole(Role.LIBRARY_ADMIN.name()) ⑤
.pathMatchers(HttpMethod.POST, "/books/{bookId}/borrow")
.hasRole(Role.LIBRARY_USER.name())
.pathMatchers(HttpMethod.POST, "/books/{bookId}/return")
.hasRole(Role.LIBRARY_USER.name()) ⑥
.pathMatchers(HttpMethod.POST, "/books")
.hasRole(Role.LIBRARY_CURATOR.name()) ⑦
.pathMatchers(HttpMethod.DELETE, "/books")
.hasRole(Role.LIBRARY_CURATOR.name())
.pathMatchers("/users/**")
.hasRole(Role.LIBRARY_ADMIN.name()) ⑧
.anyExchange()
.authenticated() ⑨
.and()
.httpBasic()
.and()
.formLogin() ⑩
.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler()) ⑪
.and()
.build();
}
@Bean ⑫
public ServerLogoutSuccessHandler logoutSuccessHandler() {
RedirectServerLogoutSuccessHandler logoutSuccessHandler = new
RedirectServerLogoutSuccessHandler();
logoutSuccessHandler.setLogoutSuccessUrl(URI.create("/books"));
return logoutSuccessHandler;
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
② Configures authentication and web layer authorization for all URL’s of our REST api
③ All static resources (favicon.ico, css, images, …) can be accessed without authentication
④ Actuator endpoints for health and info can be accessed without authentication
33
⑥ Borrow or returning books require authenticated user having the 'LIBRARY_USER' role
⑦ Modifying access to books require authenticated user having the 'LIBRARY_CURATOR' role
⑪ After logging out it redirects to URL configured in the logout success handler
We also add a a ServerLogoutSuccessHandler bean to redirect back to the /books endpoint after a
logout to omit the error message we got so far by redirecting to a non-existing page.
We continue with authorization on the method layer by adding the rules to our business service
classes BookService and UserService. To achieve this we use the @PreAuthorize annotations
provided by spring security. Same as other spring annotations (e.g. @Transactional) you can put
@PreAuthorize annotations on global class level or on method level.
Depending on your authorization model you may use @PreAuthorize to authorize using static roles
or to authorize using dynamic expressions (usually if you have roles with permissions).
If you want to have a permission based authorization you can use the predefined interface
PermissionEvaluator inside the @PreAuthorize annotations like this:
34
class MyService {
@PreAuthorize("hasPermission(#uuid, 'user', 'write')")
void myOperation(UUID uuid) {...}
}
PermissionEvaluator class
package org.springframework.security.access;
...
public interface PermissionEvaluator extends AopInfrastructureBean {
In the workshop due to time constraints we have to keep things simple so we just use static roles.
Here it is done for the all operations of the book service.
35
BookService class
package com.example.library.server.business;
...
import org.springframework.security.access.prepost.PreAuthorize;
...
@Service
@PreAuthorize("hasAnyRole('LIBRARY_USER', 'LIBRARY_CURATOR')") ①
public class BookService {
...
@PreAuthorize("hasRole('LIBRARY_CURATOR')") ②
public Mono<Void> create(Mono<BookResource> bookResource) {
return bookRepository.insert(bookResource.map(this::convert)).then();
}
...
@PreAuthorize("hasRole('LIBRARY_CURATOR')") ③
public Mono<Void> deleteById(UUID uuid) {
return bookRepository.deleteById(uuid).then();
}
...
@PreAuthorize("hasRole('LIBRARY_USER')") ④
public Mono<Void> borrowById(UUID bookIdentifier, UUID userIdentifier) {
...
}
...
@PreAuthorize("hasRole('LIBRARY_USER')") ⑤
public Mono<Void> returnById(UUID bookIdentifier, UUID userIdentifier) {
...
}
}
① In general all users (having either of these 2 roles) can access RESTful services for books
② Only users having role 'LIBRARY_CURATOR' can access this RESTful service to create books
③ Only users having role 'LIBRARY_CURATOR' can access this RESTful service to delete books
④ Only users having role 'LIBRARY_USER' can access this RESTful service to borrow books
⑤ Only users having role 'LIBRARY_USER' can access this RESTful service to return books
And now we add it the same way for the all operations of the user service.
36
UserService class
package com.example.library.server.business;
...
import org.springframework.security.access.prepost.PreAuthorize;
...
@Service
@PreAuthorize("hasRole('LIBRARY_ADMIN')") ①
public class UserService {
...
@PreAuthorize("isAnonymous() or isAuthenticated()") ②
public Mono<UserResource> findOneByEmail(String email) {
return userRepository.findOneByEmail(email).map(this::convert);
}
...
}
① In general only users having role 'LIBRARY_ADMIN' can access RESTful services for users
Now that we have the current user context available in our application we can use this to
automatically set this user as the one who has borrowed a book or returns his borrowed book. The
current user can always be evaluated using the ReactiveSecurityContextHolder class. But a more
elegant way is to just let the framework put the current user directly into our operation via
@AuthenticationPrincipal annotation.
37
BookRestController class
package com.example.library.server.api;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@RestController
public class BookRestController {
...
...
}
① Now that we have an authenticated user context we can add the current user as the one to
borrow a book
② Now that we have an authenticated user context we can add the current user as the one to
return his borrowed a book
So please go ahead and re-start the application and try to borrow a book with an authenticated
user.
Note: Replace {bookId} with the id of one of the books you have got in the list of books.
38
curl get list of books
Note: Replace {bookId} with the id of one of the books you have got in the list of books.
At first you will notice that even with the correct basic authentication header you get an error
message like this one:
POST http://localhost:8080/books
The library-server expects a CSRF token in the request but did not find one. If you use common UI
frameworks like Thymeleaf or JSF (on the serverside) or a clientside one like Angular then these
already handle this CSRF processing.
In our case we do not have such handler. To successfully tra the borrow book request you have to
switch off CSRF in the library server.
This is done like this in the WebSecurityConfiguration class.
39
Disable CSRF
...
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable() ①
.authorizeExchange()
.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
...
Restart the application and retry to borrow a book. This time the request should be successful.
Do not disable CSRF on productive servers if you use session cookies, otherwise
you are vulnerable to CSRF attacks. You may safely disable CSRF for servers that
use a stateless authentication approach with bearer tokens like for OAuth2 or
OpenID Connect.
In this workshop step we added the authorization to web and method layers. So now for particular
RESTful endpoints access is only permitted to users with special roles.
But how do you know that you have implemented all the authorization rules and did not leave a big
security leak for your RESTful API? Or you may change some authorizations later by accident?
To be on a safer side here you need automatic testing. Yes, this can also be done for security! We
will see how this works in the next workshop part.
40
Figure 16. Automated security tests
The tests will be implemented using the new JUnit 5 version as Spring 5.0 now supports this as well.
In BookServiceTest class we also use the new convenience annotation @SpringJUnitConfig which is
a shortcut of @ExtendWith(value=SpringExtension.class) and @ContextConfiguration.
As you can see in the following code only a small part is shown as a sample here to test the
BookService.create() operation. Authorization should always be tested for positive AND negative
test cases. Otherwise you probably miss an authorization constraint. Depending on the time left in
the workshop you can add some more test cases as you like or just look into the completed
application 04-library-server.
BookServiceAuthorizationTest class
package com.example.library.server.business;
...
41
UUID.randomUUID(),
"123456789",
"title",
"description",
Collections.singletonList("author"),
false,
null))))
.verifyComplete();
}
...
① As this is a JUnit 5 based integration test we use @SpringJUnitConfig to add spring JUnit 5
42
extension and configure the application context
③ Positive test case of access control for creating books with role 'LIBRARY_CURATOR'
④ Negative test case of access control for creating books with roles 'LIBRARY_USER' or
'LIBRARY_ADMIN'
⑤ Negative test case of access control for creating books with anonymous user
For sure you have to add similar tests as well for the user part.
UserServiceAuthorizationTest class
package com.example.library.server.business;
...
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
43
.thenReturn(
Mono.just(
new User(
UUID.randomUUID(),
"[email protected]",
"secret",
"Max",
"Maier",
Collections.singletonList(Role.LIBRARY_USER))));
StepVerifier.create(userService.findOneByEmail("[email protected]"))
.expectNextCount(1)
.verifyComplete();
}
...
① As this is a JUnit 5 based integration test we use @SpringJUnitConfig to add spring JUnit 5
extension and configure the application context
② Positive test case of access control for finding a user by email for anonymous user
③ Positive test case of access control for finding a user by email with all possible roles
④ Negative test case of access control for creating user with roles 'LIBRARY_USER' or
'LIBRARY_CURATOR'
Make sure you always add positive and negative authorization tests. Otherwise
you may miss authorization endpoint leaks.
Another approach is to test the authentication for the reactive api. This is shown in following class.
BookApiAuthenticationTest
44
package com.example.library.server.api;
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = BookApiAuthenticationTest.TestConfig.class) ①
@DisplayName("Access to book api")
class BookApiAuthenticationTest {
@BeforeEach
void setUp() {
this.webTestClient =
WebTestClient.bindToApplicationContext(applicationContext)
.apply(springSecurity()) ②
.configureClient()
.build();
}
@ComponentScan(
basePackages = {
"com.example.library.server.api",
"com.example.library.server.business",
"com.example.library.server.config"
})
@EnableWebFlux
@EnableWebFluxSecurity
@EnableAutoConfiguration( ③
exclude = {
MongoReactiveAutoConfiguration.class,
MongoAutoConfiguration.class,
MongoDataAutoConfiguration.class,
EmbeddedMongoAutoConfiguration.class,
MongoReactiveRepositoriesAutoConfiguration.class,
MongoRepositoriesAutoConfiguration.class
})
static class TestConfig {}
@WithMockUser ④
@Test
@DisplayName("to get list of books")
void verifyGetBooksAuthenticated() {
45
given(bookService.findAll()).willReturn(Flux.just(BookBuilder.book().build()));
webTestClient
.get()
.uri("/books")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isOk()
.expectHeader() ⑤
.exists("X-XSS-Protection")
.expectHeader()
.valueEquals("X-Frame-Options", "DENY");
}
@Test
@DisplayName("to get single book")
void verifyGetBookAuthenticated() {
given(bookService.findById(bookId))
.willReturn(Mono.just(BookBuilder.book().withId(bookId).build()));
webTestClient
.mutateWith(mockUser()) ⑥
.get()
.uri("/books/{bookId}", bookId)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isOk();
}
...
}
@Test
@DisplayName("to get list of books")
void verifyGetBooksUnAuthenticated() {
webTestClient
.get()
.uri("/books")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
46
.isUnauthorized(); ⑦
}
...
}
③ Exclude complete persistence layer from test (out of scope for authentication)
⑦ Negative test case to verify unauthenticated user is not authorized to use the api
The testing part is the last part of adding simple username/password based security to the reactive
style of the library-server project. The next step will dive into the world of token-based
authentication.
47
Chapter 7. OAuth2/OpenID Connect Labs
7.1. Introduction
7.1.1. OAuth 2.0
In the last workshop part we will look at the new OAuth2 login client and resource server introduced in
Spring Security 5.0 and 5.1.
OAuth 2.0 is the base protocol for authorizing 3rd party authentication services for using business
services in the internet like stackoverflow.
Authorizations are permitted via scopes that the user has to confirm before using the requested
service.
Depending on the application type OAuth 2.0 provides the following grants (flows):
• Implicit Grant
The following picture shows the mechanics of the Authorization Code Grant Flow.
48
Figure 18. Authorization code grant flow
OpenID Connect 1.0 (OIDC) is build upon OAuth2 and provides additional identity information to
OAuth2. For common enterprise applications that typically require authentication OpenID Connect
should be used. OIDC adds JSON web tokens (JWT) as mandatory format for id tokens to the spec. In
OAuth2 the format of bearer tokens is not specified.
OIDC adds an id token in addition to the access token of OAuth2 and specifies a user info endpoint
to retrieve further user information using the access token.
• Implicit Flow
• Hybrid Flow
1. Self-contained token (containing all information directly in token payload, e.g. JWT)
49
7.1.4. OAuth2/OIDC in Spring Security 5
Spring Security 5.0 introduced new support for OAuth2/OpenID Connect (OIDC) directly in spring security.
In short Spring Security 5.0 adds a completely rewritten implementation for OAuth2/OIDC which
now is largely based on a third party library Nimbus OAuth 2.0 SDK instead of implementing all
these functionality directly in Spring itself.
Spring Security 5.0 only provides the client side for servlet-based clients.
Spring Security 5.1 adds the resource server support and reactive support for reactive clients and
resource server as well.
Spring Security 5.2 adds client support for authorization code flow with PKCE.
Spring Security 5.3 will add a basic OAuth2/OIDC authorization server again (for local dev and
demos but not for productive use).
Before Spring Security 5.0 and Spring Boot 2.0 to implement OAuth2 you needed the separate
project module Spring Security OAuth2.
Now things have changed much, so it heavily depends now on the combination of Spring Security
and Spring Boot versions that are used how to implement OAuth2/OIDC.
Therefore you have to be aware of different approaches for Spring Security 4.x/Spring Boot 1.5.x
and Spring Security 5.x/Spring Boot 2.x.
1
Spring Boot auto-config and separate Spring Security OAuth project
2
New rewritten OAuth2 login client included in Spring Security 5.0
3
No direct support in Spring Security 5.0/Spring Boot 2.0. For auto-configuration with Spring Boot
2.0 you still have to use the separate Spring Security OAuth project together with Spring Security
OAuth2 Boot compatibilty project
4
New refactored support for resource server as part of Spring Security 5.1
5
OAuth2 login client and resource server with reactive support as part of Spring Security 5.1.
6
New OAuth2 authorization server is planned as part of Spring Security 5.2
50
You can find more information on building OAuth2 secured microservices with Spring Boot 1.5.x in
You can find more information on building OAuth2 secured microservices with Spring Boot 2.1 and
Spring Security 5.1 in
In this workshop we will now look at what Spring Security 5.1 provides as new OAuth2/OIDC Login
Client and Resource Server - In a reactive way.
• initial-resource-server: The initial library server (almost similar to workshop step lab-
1/initial-library-server)
• initial-oidc-client: Initial code for this workshop part to implement the new OAuth2 Login
Client
51
Figure 20. Library client, service and identity provider service
These micro-services have to be configured to be reachable via the following URL addresses (Port
8080 is the default port in spring boot).
Service URL
Identity Management Service (Keycloak) http://localhost:8080/auth
Library Client (OIDC Client) http://localhost:9090
Library Server (OIDC Resource Server) http://localhost:9091
So now let’s start. Again, you will just use the provided keycloak identity management server, the
lab-5/initial-resource-server and the lab-6/initial-oidc-client as starting point and implement an
OAuth2/OIDC resource server and client based on the project.
But first read important information about how to setup and start the required keycloak identity
management server.
To setup the local keycloak server please copy/extract it from provided USB sticks or follow the
setup instructions at https://tinyurl.com/y2mqyeua.
You may look into OpenID Connect certified products to find a suitable identity
management server for your project.
Every OpenID Connect 1.0 compliant identity server must provide a page at the endpoint /…/.well-
known/openid-configuration
52
To see the configuration please open the following url in your web browser: Well known OIDC
configuration
To login into your local keycloak use the following user credentials:
• Username: admin
• Password: admin
The keycloak identity service has been preconfigured with the following user credentials for the
workshop application:
Please make sure that you have started keycloak server. Then check this out, go to project intro-
53
labs/auth-code-demo and run the class
com.example.authorizationcode.client.AuthorizationCodeDemo.
To use the new OAuth2 resource server support of Spring Security 5.1 you have to add the following
required dependencies to the existing gradle build file.
gradle.build file
dependencies {
...
implementation('org.springframework.boot:spring-boot-starter-oauth2-resource-
server') ①
testImplementation('org.springframework.security:spring-security-test') ②
...
}
① This contains all code to build an OAuth 2.0/OIDC resource server (incl. support for JOSE
(Javascript Object Signing and Encryption)
You may look into the spring security oauth2 boot reference documentation Spring
Boot 2.1 Reference Documentation and the Spring Security 5.1 Reference
Documentation on how to implement a resource server.
First step is to configure an OAuth2 resource server. For this you have to register the corresponding
identity server/authorization server to use.
54
{
"issuer": "http://localhost:8080/auth/realms/workshop",
"authorization_endpoint":
"http://localhost:8080/auth/realms/workshop/protocol/openid-connect/auth",
"token_endpoint": "http://localhost:8080/auth/realms/workshop/protocol/openid-
connect/token",
"userinfo_endpoint": "http://localhost:8080/auth/realms/workshop/protocol/openid-
connect/userinfo",
"jwks_uri": "http://localhost:8080/auth/realms/workshop/protocol/openid-
connect/certs"
}
For configuring a resource server the important entries are issuer and jwks_uri. Spring Security 5
automatically configures a resource server by just specifying the issuer uri value as part of the
predefined spring property spring.security.oauth2.resourceserver.jwt.issuer-uri
application.yml file
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/auth/realms/workshop ①
① The issuer url is used to look up the well known configuration page to get all required
configuration settings to set up a resource server
An error you get very often with files in yaml format is that the indents are not
correct. This can lead to unexpected errors later when you try to run all this stuff.
With this configuration in place we have already a working resource server that can handle JWt
access tokens transmitted via http bearer token header. Spring Security also validates by default:
• the JWT signature against the queried public key(s) from jwks_url
The issuer URI is used to retrieve the well known OpenID Connect configuration.
55
http://localhost:8080/auth/realms/workshop/.well-known/openid-configuration
{
"issuer": "http://localhost:8080/auth/realms/workshop",
"authorization_endpoint":
"http://localhost:8080/auth/realms/workshop/protocol/openid-connect/auth",
"token_endpoint": "http://localhost:8080/auth/realms/workshop/protocol/openid-
connect/token",
"token_introspection_endpoint":
"http://localhost:8080/auth/realms/workshop/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "http://localhost:8080/auth/realms/workshop/protocol/openid-
connect/userinfo",
"end_session_endpoint": "http://localhost:8080/auth/realms/workshop/protocol/openid-
connect/logout",
"jwks_uri": "http://localhost:8080/auth/realms/workshop/protocol/openid-
connect/certs",
"check_session_iframe": "http://localhost:8080/auth/realms/workshop/protocol/openid-
connect/login-status-iframe.html",
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password",
"client_credentials"
],
"response_types_supported": [
"code",
"none",
"id_token",
"token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
...
}
The web security configuration looks like the ones we have seen before with all that authorization
rule settings. The addition here is just for replacing the basic authentication with bearer token
authentication (expected in the http header). Additionally there are two possible alternative JWT
converters referenced there.
Usually this configuration would be sufficient but as we also want to make sure that our resource
server is working with stateless token authentication we have to configure stateless sessions (i.e.
prevent JSESSION cookies). Starting with Spring Boot 2 you always have to configure Spring
Security yourself as soon as you introduce a class that extends WebSecurityConfigurerAdapter.
WebSecurityConfiguration.java file
56
package com.example.library.server.config;
import com.example.library.server.common.Role;
import com.example.library.server.security.LibraryReactiveUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest;
import org.springframework.boot.autoconfigure.security.reactive.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import
org.springframework.security.config.annotation.method.configuration.EnableReactiveMeth
odSecurity;
import
org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class WebSecurityConfiguration {
@Autowired
public WebSecurityConfiguration(
LibraryReactiveUserDetailsService libraryReactiveUserDetailsService) {
this.libraryReactiveUserDetailsService = libraryReactiveUserDetailsService;
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf()
.disable()
.authorizeExchange()
.matchers(PathRequest.toStaticResources().atCommonLocations())
.permitAll()
.matchers(EndpointRequest.to("health"))
.permitAll()
.matchers(EndpointRequest.to("info"))
.permitAll()
.matchers(EndpointRequest.toAnyEndpoint())
.hasRole(Role.LIBRARY_ADMIN.name())
.pathMatchers(HttpMethod.POST, "/books/{bookId}/borrow")
.hasRole(Role.LIBRARY_USER.name())
.pathMatchers(HttpMethod.POST, "/books/{bookId}/return")
57
.hasRole(Role.LIBRARY_USER.name())
.pathMatchers(HttpMethod.POST, "/books")
.hasRole(Role.LIBRARY_CURATOR.name())
.pathMatchers(HttpMethod.DELETE, "/books")
.hasRole(Role.LIBRARY_CURATOR.name())
.pathMatchers("/users/**")
.hasRole(Role.LIBRARY_ADMIN.name())
.anyExchange()
.authenticated()
.and()
.oauth2ResourceServer() ①
.jwt() ②
.jwtAuthenticationConverter(libraryUserJwtAuthenticationConverter()); ③
// .jwtAuthenticationConverter(libraryUserRolesJwtAuthenticationConverter());
return http.build();
}
@Bean
public LibraryUserJwtAuthenticationConverter libraryUserJwtAuthenticationConverter()
{
return new
LibraryUserJwtAuthenticationConverter(libraryReactiveUserDetailsService);
}
@Bean
public LibraryUserRolesJwtAuthenticationConverter
libraryUserRolesJwtAuthenticationConverter() {
return new
LibraryUserRolesJwtAuthenticationConverter(libraryReactiveUserDetailsService);
}
}
② Configures JSON web token (JWT) handling for this resource server
• disables CSRF protection (without session cookies we do not need this any more) (which also
makes it possible to make post requests on the command line)
• enables this as a resource server with expecting access tokens in JWT format
With mapping user information like roles you always have the choice between
• Getting the roles information from the mapped local persistent user
58
The converter for getting roles from JWT token looks like the following:
LibraryUserJwtAuthenticationConverter.java file
package com.example.library.server.config;
import com.example.library.server.security.LibraryReactiveUserDetailsService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.Collections;
import java.util.stream.Collectors;
/** JWT converter that takes the roles from 'groups' claim of JWT token. */
public class LibraryUserJwtAuthenticationConverter
implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
private static final String GROUPS_CLAIM = "groups";
private static final String ROLE_PREFIX = "ROLE_";
public LibraryUserJwtAuthenticationConverter(
LibraryReactiveUserDetailsService libraryReactiveUserDetailsService) {
this.libraryReactiveUserDetailsService = libraryReactiveUserDetailsService;
}
@Override
public Mono<AbstractAuthenticationToken> convert(Jwt jwt) { ①
Collection<GrantedAuthority> authorities = extractAuthorities(jwt);
return libraryReactiveUserDetailsService
.findByUsername(jwt.getClaimAsString("email"))
.map(u -> new UsernamePasswordAuthenticationToken(u, "n/a", authorities));
}
@SuppressWarnings("unchecked")
private Collection<String> getScopes(Jwt jwt) { ③
Object scopes = jwt.getClaims().get(GROUPS_CLAIM);
59
if (scopes instanceof Collection) {
return (Collection<String>) scopes;
}
return Collections.emptyList();
}
}
① Map JWT to Authentication object with matching user and roles (Authorities) from JWT token
The converter for using the roles from the mapped local user looks like this:
LibraryUserRolesJwtAuthenticationConverter.java file
package com.example.library.server.config;
import com.example.library.server.security.LibraryReactiveUserDetailsService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.jwt.Jwt;
import reactor.core.publisher.Mono;
/** JWT converter that takes the roles from persistent user roles. */
public class LibraryUserRolesJwtAuthenticationConverter
implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
public LibraryUserRolesJwtAuthenticationConverter(
LibraryReactiveUserDetailsService libraryReactiveUserDetailsService) {
this.libraryReactiveUserDetailsService = libraryReactiveUserDetailsService;
}
@Override
public Mono<AbstractAuthenticationToken> convert(Jwt jwt) { ①
return libraryReactiveUserDetailsService
.findByUsername(jwt.getClaimAsString("email"))
.map(u -> new UsernamePasswordAuthenticationToken(u, "n/a",
u.getAuthorities()));
}
}
① Map JWT to Authentication object with matching user and roles (Authorities) from user as well
To start the resource server simply run the class LibraryServerApplication in project lab-
5/complete-resource-server.
60
In the following paragraphs we now proceed to the client side of the OAuth2/OIDC part.
To use the new OAuth2 client support of Spring Security 5.1 you have to add the following required
dependencies to the existing gradle build file.
gradle.build file
dependencies {
...
implementation('org.springframework.boot:spring-boot-starter-oauth2-client') ①
...
}
① Spring Boot starter for OAuth2 client including core OAuth2/OIDC client and JOSE (Javascript
Object Signing and Encryption) framework to support for example JSON Web Token (JWT)
First step is to configure an OAuth2/OIDC client. For this you have to register the corresponding
identity server/authorization server to use. Here you have two possibilities:
1. You can just use one of the predefined ones (Facebook, Google, etc.)
61
CommonOAuth2Provider class
package org.springframework.security.config.oauth2.client;
...
public enum CommonOAuth2Provider {
GOOGLE {
@Override
public Builder getBuilder(String registrationId) {
ClientRegistration.Builder builder = getBuilder(registrationId,
ClientAuthenticationMethod.BASIC, DEFAULT_LOGIN_REDIRECT_URL);
builder.scope("openid", "profile", "email");
builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
builder.userNameAttributeName(IdTokenClaimNames.SUB);
builder.clientName("Google");
return builder;
}
},
GITHUB {
@Override
public Builder getBuilder(String registrationId) {
...
}
},
FACEBOOK {
@Override
public Builder getBuilder(String registrationId) {
...
}
},
...
}
To use one of these providers is quite easy. Just reference the enumeration constant as the provider.
62
Google provider properties class
spring:
security:
oauth2:
client:
registration:
google-login: ①
provider: google ②
client-id: google-client-id
client-secret: google-client-secret
You can find a sample application using the common provider for GitHub in
project intro-labs/github-client.
But in this workshop we will focus on the second possibility and use our own custom identity
provider service.
To achieve this we add the following sections to the application.yml file.
For configuring an OAuth2 client the important entries are issuer, authorization_endpoint,
token_endpoint, userinfo_endpoint and jwks_uri. Spring Security 5 automatically configures an
OAuth2 client by just specifying the issuer uri value as part of the predefined spring property
spring.security.oauth2.client.provider.[id].issuer-uri.
For OAuth2 clients you always have to specify the client registration (with client id, client secret,
authorization grant type, redirect uri to your client callback and optionally the scope). The client
registration requires an OAuth2 provider. If you want to use your own provider you have to
configure at least the issuer uri. We want to change the default user name mapping for the user
identity as well ( using the user name instead of the default value 'sub').
63
application.yml client configuration
server:
port: 9090
spring:
security:
oauth2:
client:
registration:
keycloak: ①
client-id: 'library-client'
client-secret: '9584640c-3804-4dcd-997b-93593cfb9ea7'
authorizationGrantType: authorization_code
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
scope: openid
provider:
keycloak:
issuerUri: http://localhost:8080/auth/realms/workshop ②
user-name-attribute: name
① Client configuration like client-id and client-secret credentials and where to redirect to
② The issuer url is used to look up the well known configuration page to get all required
configuration settings to set up a client
As the library-server is now configured as an OAuth2 resource server it requires a valid JWT token
to successfully call the /books endpoint now.
For all requests to the resource server we use the reactive web client, that was introduced by Spring
5. WebClient is the successor of RestTemplate and works for both worlds (Servlet-based and
reactive).
The next required step is to make this web client aware of transmitting the required bearer access
tokens in the Authorization header.
To support JWT tokens in calls we have to add a client interceptor to the WebClient. The following
code snippet shows how this is done:
64
WebClientConfiguration class
package com.example.oauth2loginclient.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import
org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepo
sitory;
import
org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2Au
thorizedClientExchangeFilterFunction;
import
org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepo
sitory;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfiguration {
@Bean
WebClient webClient(ReactiveClientRegistrationRepository
clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository
authorizedClientRepository) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = ①
new
ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository,
authorizedClientRepository);
oauth.setDefaultOAuth2AuthorizedClient(true); ②
oauth.setDefaultClientRegistrationId("keycloak"); ③
return WebClient.builder()
.filter(oauth) ④
.build();
}
}
① Creates a filter for handling all the OAuth2 token stuff (i.e. initiating the OAuth2 code grant flow)
② Set this OAuth2 client as default for all requests (Do not set this if you have requests that do not
require access tokens)
With this additions we add a filter function to the web client that automatically adds the access
token to all requests and also initiates the authorization grant flow if no valid access token is
available.
Finally we need an updated client side security configuration to allow client endpoints and enable
65
the OAuth2 client features:
SecurityConfiguration class
package com.example.oauth2loginclient.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import
org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@EnableWebFluxSecurity
@Configuration
public class SecurityConfiguration {
@Bean
SecurityWebFilterChain configure(ServerHttpSecurity http) {
http.authorizeExchange().anyExchange().authenticated().and().oauth2Login().and().oauth
2Client(); ①
return http.build();
}
}
The client is build as a Thymeleaf web client. Thymeleaf basically works with HTML template files
with some specials tags to connect the template with Spring beans.
• index.html: The main template that is displayed initially to show list of books
• users.html: This shows the list of users retrieved from the library server
To map these HTML template files to the web request paths and also map the content (the 'model'
as it is called in Spring MVC) a controller class (annotated with @Controller) is required.
BookController class
package com.example.oidc.client.api;
import com.example.oidc.client.api.resource.BookResource;
import com.example.oidc.client.api.resource.CreateBookResource;
import org.springframework.beans.factory.annotation.Value;
66
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.IOException;
@Controller
public class BookController { ①
@Value("${library.server}")
private String libraryServer;
@ModelAttribute("books")
Flux<BookResource> books() { ②
return webClient
.get()
.uri(libraryServer + "/books")
.retrieve()
.onStatus(
s -> s.equals(HttpStatus.UNAUTHORIZED),
cr -> Mono.just(new BadCredentialsException("Not authenticated")))
.onStatus(
s -> s.equals(HttpStatus.FORBIDDEN),
cr -> Mono.just(new AccessDeniedException("Not authorized")))
.onStatus(
HttpStatus::is4xxClientError,
cr -> Mono.just(new
IllegalArgumentException(cr.statusCode().getReasonPhrase())))
.onStatus(
HttpStatus::is5xxServerError,
cr -> Mono.just(new Exception(cr.statusCode().getReasonPhrase())))
.bodyToFlux(BookResource.class);
}
67
@GetMapping("/")
Mono<String> index(@AuthenticationPrincipal OAuth2User user, Model model) { ③
model.addAttribute("fullname", user.getName());
model.addAttribute(
"isCurator",
user.getAuthorities().stream().anyMatch(ga ->
ga.getAuthority().equals("library_curator")));
return Mono.just("index");
}
@GetMapping("/createbook")
String createForm(Model model) { ④
return "createbookform";
}
@PostMapping("/create")
Mono<String> create( ⑤
CreateBookResource createBookResource, ServerWebExchange serverWebExchange,
Model model)
throws IOException {
return webClient
.post()
.uri(libraryServer + "/books")
.body(Mono.just(createBookResource), CreateBookResource.class)
.retrieve()
.onStatus(
s -> s.equals(HttpStatus.UNAUTHORIZED),
cr -> Mono.just(new BadCredentialsException("Not authenticated")))
.onStatus(
s -> s.equals(HttpStatus.FORBIDDEN),
cr -> Mono.just(new AccessDeniedException("Not authorized")))
.onStatus(
HttpStatus::is4xxClientError,
cr -> Mono.just(new
IllegalArgumentException(cr.statusCode().getReasonPhrase())))
.onStatus(
HttpStatus::is5xxServerError,
cr -> Mono.just(new Exception(cr.statusCode().getReasonPhrase())))
.bodyToMono(BookResource.class)
.then(Mono.just("redirect:/"));
}
@GetMapping("/borrow") ⑥
Mono<String> borrowBook(@RequestParam("identifier") String identifier) {
return webClient
68
.post()
.uri(libraryServer + "/books/{bookId}/borrow", identifier)
.retrieve()
.onStatus(
s -> s.equals(HttpStatus.UNAUTHORIZED),
cr -> Mono.just(new BadCredentialsException("Not authenticated")))
.onStatus(
s -> s.equals(HttpStatus.FORBIDDEN),
cr -> Mono.just(new AccessDeniedException("Not authorized")))
.onStatus(
HttpStatus::is4xxClientError,
cr -> Mono.just(new
IllegalArgumentException(cr.statusCode().getReasonPhrase())))
.onStatus(
HttpStatus::is5xxServerError,
cr -> Mono.just(new Exception(cr.statusCode().getReasonPhrase())))
.bodyToMono(BookResource.class)
.then(Mono.just("redirect:/"));
}
@GetMapping("/return")
Mono<String> returnBook( ⑦
@RequestParam("identifier") String identifier, ServerWebExchange
serverWebExchange) {
return webClient
.post()
.uri(libraryServer + "/books/{bookId}/return", identifier)
.retrieve()
.onStatus(
s -> s.equals(HttpStatus.UNAUTHORIZED),
cr -> Mono.just(new BadCredentialsException("Not authenticated")))
.onStatus(
s -> s.equals(HttpStatus.FORBIDDEN),
cr -> Mono.just(new AccessDeniedException("Not authorized")))
.onStatus(
HttpStatus::is4xxClientError,
cr -> Mono.just(new
IllegalArgumentException(cr.statusCode().getReasonPhrase())))
.onStatus(
HttpStatus::is5xxServerError,
cr -> Mono.just(new Exception(cr.statusCode().getReasonPhrase())))
.bodyToMono(BookResource.class)
.then(Mono.just("redirect:/"));
}
}
69
Render the form to create new book
⑤ Use reactive webclient to call POST 'books' endpoint on library resource server to create book
⑥ Use reactive webclient to call POST 'books' endpoint on library resource server to borrow a book
⑦ Use reactive webclient to call POST 'books' endpoint on library resource server to return a book
In the client you can see the contents of the ID JWT token as well using the '/userinfo' endpoint. This
endpoint is mapped to a @RestController.
UserInfoRestController class
package com.example.oauth2loginclient.api;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.util.Map;
@RestController
public class UserInfoRestController {
@GetMapping("/userinfo")
Mono<Map<String, Object>> userInfo(@AuthenticationPrincipal OAuth2User oauth2User) {
return Mono.just(oauth2User.getAttributes()); ①
}
}
Now when you access localhost:9090/userinfo you should be redirected to the keycloak identity
server. After logging in you should get the current authenticated user info back from identity
server.
70
bbanner bruce.banner@exampl banner LIBRARY_USER
e.com
pparker peter.parker@example. parker LIBRARY_CURATOR
com
ckent [email protected] kent LIBRARY_ADMIN
m
You can now access localhost:9090 as well. This returns the book list from the library-server
(resource server).
Logout Users
After you have logged in into the library client using keycloak your session will remain valid until
the access token has expired or the session at keycloak is invalidated.
As the library client does not have a logout functionality, you have to follow the following steps to
actually log out users:
• Login to keycloak admin console and navigate on the left to menu item session Here you’ll see
all user sessions (active/offline ones). By clicking on button Logout all you can revocate all
active sessions.
• After you have revocated sessions in keycloak you have to delete the current JSESSION cookie
for the library client. You can do this by opening the application tab in the developer tools of
chrome. Navigate to the cookies entry on the left and select the url of the library client, then
delete the cookie on the right hand side
Now when you refresh the library client in the browser you should be redirected again to the login
page of keycloak.
This concludes our Spring Security 5.1 hands-on workshop. I hope you have learned a lot regarding
71
security and especially Spring Security 5.1.
So take the next step and make YOUR applications more secure !
72
Chapter 8. Feedback
If you have further feedback for this workshop, suggestions for improvements or you want me to
conduct this workshop somewhere else please do not hesitate to contact me via
• Mail: [email protected]
• Twitter: @andifalk
• LinkedIn: andifalk
Thank YOU very much for being part of this workshop :-)
73
References
▪ OWASP Top 10 2017
▪ OAuth2 Specifications
74
Appendix A: Copyright and License
Copyright © 2019 by Andreas Falk.
Free use of this software is granted under the terms of the Apache 2.0 License.
75