Reactive Spring Security For WebFlux REST Web Services - II
So, let's continue our discussion from the earlier post on WebFlux REST API Security.
Configuring Reactive Security
We'll begin from the sample in the earlier post, i.e. this one:
Configuring Request Level Security
As you'd have guessed, the .authorizeExchange() portion above is for configuring security at the request level. anyExchange() above would match any request, and .authenticated() would restrict the matched requests only to authenticated users. Here is another snippet with some more examples:
It says that all DELETE requests are blocked, /login and /logout are permitted to all users, GET /managers-can-see-this-folder/** and GET /and-this-page are permitted only to MANAGERs, any request with Accept: application/pdf header is only for ADMINs, and all other requests are only for authenticated users. Spring security would traverse the matchers sequentially, and stop at the first matcher that matches.
The .matchers(exchange -> ...) above is a powerful matcher, taking one or more matchers of type ServerWebExchangeMatcher. So, we could code here custom matchers with whatever logic we want. The MediaTypeServerWebExchangeMatcher used above is a predefined matcher provided by Spring Security. There are many other predefined matchers, like the OrServerWebExchangeMatcher.
Getting back to our case, if we want to open up all the endpoints (and deal with authorization in method security), just the following will do:
No HTTP Basic Authentication
The above sample configures both basic and form authentication. Let's remove the basic one — form authentication should suffice our requirements. To do so, just remove .httpBasic().and().
Configuring Form Authentication
Remember our login requirements? It was "a user should be able to call POST /login with username and password to receive a token."
To configure this, expand the formLogin() configuration above to
The .loginPage("/login") above tells Spring Security to make the endpoint available at /login. It's actually the default, so ideally we can omit it. But there's a bug in Spring Security that overwrites some configurations (in the lines below) if we omit it, so let it be there.
The next line, .authenticationFailureHandler(...), tells what to do if there's an authentication exception, e.g. the user is not found or the password is incorrect. The default behavior is to redirect the user (to an authentication-failure-url). But in REST APIs. we'd like to return a 401 Unauthorized response. That's why we have provided above a ServerAuthenticationFailureHandler that returns an error Mono, which can then be translated to a 401 response by our exception handling mechanism.
The next line, .authenticationSuccessHandler(...), tells Spring Security to proceed with successful login requests to a /login handler(i.e. controller method), instead of the default behavior of redirecting to a success-url. The idea is that, in the "/login" handler, we could return a 200 OK response with a Bearer token. The handler could look something like this:
In summary, the above code returns a 200 response with our MyUserDetails as the body. It also adds a bearer token in a response header (the addTokenHeader should do that).
In your scenario though, you may like to have it differently — e.g. or you may like to return the token in the body, or may not like to return the raw MyUserDetails. Here is a real world example from Spring Lemon, which takes an optional expirationMillis parameter and returns a token in a response header (which expires after the given expirationMillis).
A couple of notes before we proceed:
- As you might have guessed, ReactiveSecurityContextHolder.getContext() returns the reactive security context.
- How the bearer token is created and parsed is up to you, but JWT/JWEs fit well for this purpose. Refer to this blog post and Spring Lemon's JwtService to know how exactly to use JWEs for Bearer tokens.
The security context in a WebFlux application is stored in a ServerSecurityContextRepository. Its WebSessionServerSecurityContextRepository implementation, which is used by default, stores the context in session. Configuring a NoOpServerSecurityContextRepository instead would make our application stateless. To do so, just add the following lines to the SecurityWebFilterChain configuration:
401 Unauthorized in case of authentication failure or unauthenticated access
In case of an authentication failure or unauthenticated access, by default Spring Security redirects the user to a login page where the user gets a chance to login. But in REST APIs, we'd instead like to return a 401 response. To configure so, we can add the following to our SecurityWebFilterChain configuration:
The above says Spring Security to throw exceptions in cases of authentication failure and unauthenticated access. The exception can then be translated to 401 responses by our exception handling mechanism.
One of the benefits of going stateless with Bearer tokens is that we won't need CSRF protection. So, to disable it, we can add the following to our SecurityWebFilterChain configuration:
CORS Configuration isn't completely supported yet by Reactive Security; so best to wait until Spring Security 5.1 is released, if you can.
We are stateless after all, and so won't need to logout. So, we don't need the POST /logout endpoint that's provided by Spring Security by default. To remove it, we can add the following lines to our SecurityWebFilterChain configuration:
Where we are
So, below is our SecurityWebFilterChain configuration so far:
Enabling Method Security
Enabling method security is simple; just annotate a configuration class with @EnableReactiveMethodSecurity, and then use method level annotations, such as @PreAuthorize("isAuthenticated()"), just like traditional Spring security. There's a gotcha however — reactive parameters can't be referred in the annotations. In other words, things like this won't work:
So, we have come well far! The next big thing is configuring token authentication, which we'll discuss in the next lesson.