Customized WebFlux Form Authentication
This demonstration examines Spring Security WebFlux’s Authentication mechanisms. We will look at authentication with HTML forms using Mustache, User Authentication, and customized form-based login / logout configurations.
The ServerHttpSecurity Configuration
SecurityWebFilterChain is the governing chain of [WebFilter]’s that allows us to lock down reactive WebFlux applications. With @EnableWebFluxSecurity turned on, we can build this object by issuing commands to the ServerHttpSecurity DSL object.
SecurityConfiguration.java:
@EnableWebFluxSecurity
@Slf4j
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
// ...
First, by calling authorizeExchange()
method to expose AuthorizeExchangeSpec lets us proceed with authentication details.
With this, we can apply a matcher and permission model to our endpoints. For this example, we want to open several endpoints to everyone. This is where permitAll()
can be applied to a multi-argument pathMatchers()
expression.
SecurityConfiguration.java:
.pathMatchers("/login",
"/bye",
"/favicon.ico",
"/images/**")
.permitAll()
For this example, we want every endpoint thats not publicly available to require a logged-in user. Wire in a PathMatcher for all other URL’s, and apply the authenticated()
operator to whatever matches.
SecurityConfiguration.java:
.pathMatchers("/**")
.authenticated()
.and()
CSRF Configuration
Another component for configuring SecurityWebFilterChain is the CsrfSpec enabled by calling csrf()
method. This lets us configure CSRF tokens and request matchers, or exclude CSRF entirely.
In this demo we will use the default options, so '_csrf'
becomes our parameter namd, and 'X-CSRF-TOKEN'
is our header.
SecurityConfiguration.java:
.csrf()
//.csrfTokenRepository(customCsrfTokenRepository)
//.requireCsrfProtectionMatcher(customCsrfMatcher)
.and()
Note: Currently supported token repository exists are:
Class | Function |
---|---|
WebSessionServerCsrfTokenRepository | Store CSRF token in a Web Session |
CookieServerCsrfTokenRepository | Store CSRF token in custom cookie |
Later, we will customize how CSRF tokens get included to our web page through custom filtering.
Customizing Login/logout
Spring Security provides login/logout pages on demand whenever one is not already configured. This is provided by the LoginPageGeneratingWebFilter and LogoutPageGeneratingWebFilter that get wired in if no login/logout page was specified.
To override this, expose FormLoginSpec by calling HttpServerSecurity's
formLogin()
method. We can then issue the path the our custom login page and declare form-login success/error Handlers. We want to redirect successes to the home page by using RedirectServerAuthenticationSuccessHandler. Then For logout successes, we’ll send the user to the “/bye” endpoint by configuring the RedirectServerLogoutSuccessHandler in a separate method since it’s contructor doesnt support parameters.
SecurityConfiguration.java:
.and()
.formLogin()
.loginPage("/login")
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/"))
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler("/bye"))
.and()
.build();
}
public ServerLogoutSuccessHandler logoutSuccessHandler(String uri) {
RedirectServerLogoutSuccessHandler successHandler = new RedirectServerLogoutSuccessHandler();
successHandler.setLogoutSuccessUrl(URI.create(uri));
return successHandler;
}
}
Both handlers do similar things - namely redirect on success. Expelicitly constructing the logoutSuccessHandler
since it’s constructor only allows no-args.
Next, we will look at how user/pass pairs are authenticated. This is done by a subclass of ReactiveAuthenticationManager.
Authenticating Users
UserDetailsRepositoryReactiveAuthenticationManager
bean is provided automatically if there are no other configured ReactiveAuthenticationManager @Bean
definitions. This authentication manager defers principal/credential operations to a ReactiveUserDetailsService implementation.
Spring comes with ready-made implemenations for storing and looking up users in the MapReactiveUserDetailsService. We’ll complete this section using the map reactive implementation, and by having our users come from the handly User object since no other details are necessary for customizing the user.
UserDetailBeans.java:
@Configuration
public class UserDetailServiceBeans {
@Bean
public MapReactiveUserDetailsService mapReactiveUserDetailsService() {
return new MapReactiveUserDetailsService(users);
}
private static final PasswordEncoder pw = PasswordEncoderFactories.createDelegatingPasswordEncoder();
private static UserDetails user(String u, String... roles) {
return User
.withUsername(u)
.passwordEncoder(pw::encode)
.password("pw")
.authorities(roles)
.build();
}
private static final Collection<UserDetails> users = new ArrayList<>(
Arrays.asList(
user("thor", "ROLE_USER"),
user("loki", "ROLE_USER"),
user("odin", "ROLE_ADMIN", "ROLE_USER")
));
}
View Configuration with Mustache
To enable Mustache Views, we need to wire in an appropriate ViewResolver so view names are rendered with the Mustache template View.
First, include the spring-boot-starter-mustache
dependency in your pom.xml.
pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mustache</artifactId>
</dependency>
Now, create a [WebFluxConfigurer]() to enable Mustache View rendering.
WebConfig.java:
@Configuration
class WebConfig implements WebFluxConfigurer {
private final MustacheViewResolver resolver;
// The resolver is provided by MustacheAutoConfiguration class
WebConfig(MustacheViewResolver resolver) {
this.resolver = resolver;
}
// order matters; cache will find first and render.
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.viewResolver(resolver);
}
}
This configuration will populate a MustacheViewResolver into the ViewResolverRegistry.
Mustache Web Content
Mustache lets us use includes for re-usable content. We will create 3 common fragements for our views.
frag/header.html:
<!doctype html>
<html lang="en">
<body>
frag/footer.html:
</body>
</html>
frag/logout.html:
<form class="form-inline" action="/logout" method="post">
<input type="hidden" name="_csrf" value="{{_csrf.token}}" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Escape!</button>
</form>
Next we can define the meat of our content. We just need a login, game, and logout page.
game.html:
{{>frag/header}}
<h1>West of House</h1>
<div>Hello, {{user.username}}. You are standing in an open field west of a white house, with a boarded front door.<br/>
There is a small mailbox here.</div>
<br/>
><image src="https://rawgit.com/marios-code-path/reactive-authentication/master/form-login/src/main/resources/images/cursor.gif" />
{{>frag/logout}}
{{>frag/footer}}
login-form.html:
{{>frag/header}}
<h1>Welcome to Sp0rk. This version created 21-JUL-2018</h1>
<div id="main-content" class="container">
<div class="row">
<div class="col-md-6">
<form class="form-inline" action="/login" method="post">
<div class="form-group">
<label for="username">What is your name?</label>
<input type="text" name="username" id="username" class="form-control" placeholder="My name is...">
</div>
<div class="form-group">
<label for="password">What is the passcode?</label>
<input type="password" name="password" id="password" class="form-control" placeholder="Passphrase">
</div>
<br/>
<input type="hidden" name="_csrf" value="{{_csrf.token}}" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Who R U</button>
</form>
</div>
</div>
</div>
{{>frag/footer}}
bye.html:
{{>frag/header}}
<h1>Leaving the Great Underground Empire</h1>
<div>
See you later, dungeon-master!<br/>
</div>
<br/>
{{>frag/footer}}
Configuring Mustache Behaviour
To let Mustache view resolver know where to find our templates, how to handle view objects, we set the specific options for this. Using expose-request-attributes
allows us to access our request’s view attributes from within the template.
application.properties
spring.mustache.prefix=classpath:/templates/mustache/
spring.mustache.suffix=.html
spring.mustache.expose-request-attributes=true
Routing to Views
We need to wire up our views with routing logic, so lets add this with the functional style RouterFunction.
We need a way to display icons, so first we will wire in a ‘favicon.ico’ route, and send it to a ClassPathResource resource for classpath resolution. Alternately, we could imply that the file exists on the local FileSystem by using a FileSystemResource.
WebRoutes.java:
@Component
public class WebRoutes {
@Bean
RouterFunction<?> iconResources() {
return RouterFunctions
.resources("/favicon.**", new ClassPathResource("images/favicon.ico"));
}
Next we will have a route to the login page. Since we already overrode the default location in ServerHttpSecurity
, we must provide the route to our new login page. Also included in this example is our logout landing page. it can be any URL you select, for this example we have a single page to tell the user ‘goodbye’.
WebRoutes.java:
@Bean
RouterFunction<?> viewRoutes() {
return RouterFunctions
.route(RequestPredicates.GET("/login"),
req -> ServerResponse
.ok()
.render("login-form",
req.exchange().getAttributes())
)
.andRoute(RequestPredicates.GET("/bye"),
req -> ServerResponse.ok().render("bye")
)
Route Filtering & CSRF
During ServerHttpSecurity
configuration, we added the line for csrf()
that has the effect of implementing request/response filtering. The effect of this Filter - CsrfWebFilter is to create, store and validate csrf tokens where seen or needed. We can expose the CSRF token by including the form entry ‘_csrf’ and accessing our view model to extract the token value.
Remember that the parameter and headers names may be changed by additional configuration to the CsrfSpec during configuration.
This filter allows us to access the CSRF token per request, or as needed if you are explicit about the paths that must be matched. CSRF tokens are stored as the classname org.springframework.security.web.csrf
within the request attributes map. We can access this and determine how to expose it to the view by calling the token parameterName()
method (then placing the token as that name in the model map).
.filter((req, resHandler) ->
req.exchange()
.getAttributeOrDefault(
CsrfToken.class.getName(),
Mono.empty().ofType(CsrfToken.class)
)
.flatMap(csrfToken -> {
req.exchange()
.getAttributes()
.put(csrfToken.getParameterName(), csrfToken);
return resHandler.handle(req);
})
);
}
Data Model for Views
The last route will require some information about the user logged in. We can construct the model for our mustache template by incluing a Map<String, Object>
as the second argument to the render()
method.
To get to the logged-in user, we get the principal from the ServerRequest object, cast it to it’s value type, and inject it into request attributes Map under the ‘user’ key. The attributes map (model object) is then passed in to the render()
function.
WebRoutes.java:
.andRoute(RequestPredicates.GET("/"),
req -> req.principal()
.ofType(Authentication.class)
.flatMap(auth -> {
User user = User.class.cast(auth.getPrincipal());
req.exchange()
.getAttributes()
.putAll(Collections.singletonMap("user", user));
return ServerResponse.ok().render("game",
req.exchange().getAttributes());
})
)
Application execution Entry
This simple app requires no additional configuration beyond EnableWebFlux and SpringBootApplication annotations.
App.java:
@SpringBootApplication
@EnableWebFlux
public class FormLoginApp {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(FormLoginApp.class);
app.run();
}
}
Execute the application by running it in your IDE or by executing the following comand in a bash window.
mvn spring-boot:run
Conclusion
This simple web was made to demonstratekey concepts in obtaining successful authentication from a user that is browser-bound. We looked at wiring up CSRF, Mustache views, login/logout customization, and Routing/Filtering in the WebFlux environment.