I try to authenticate user (it works) and get a user token from the context and it doesn't work.
I have a simple microservice application as a my pet-project and use a WebFlux as a web-framework. I tried to debug ReactiveSecurityContextHolder#getContext and inside flatMap I see my user token inside context, but in my application I steel have an empty context.
SecurityConfiguration.java
#EnableWebFluxSecurity
public class SecurityConfiguration {
#Bean
public SecurityWebFilterChain securityWebFilterChain(
AuthenticationWebFilter authenticationWebFilter,
ServerHttpSecurity http) {
return http
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange()
.anyExchange().authenticated()
.and()
.build();
}
#Bean
public AuthenticationWebFilter authenticationWebFilter(
SecurityTokenBasedAuthenticationManager authenticationManager,
TokenAuthenticationConverter tokenAuthenticationConverter) {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(tokenAuthenticationConverter);
authenticationWebFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
return authenticationWebFilter;
}
}
ReactiveAuthenticationManager.java
#Slf4j
#Component
#AllArgsConstructor
public class SecurityTokenBasedAuthenticationManager implements ReactiveAuthenticationManager {
private final AuthWebClient authWebClient;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.switchIfEmpty(Mono.defer(this::raiseBadCredentials))
.cast(Authorization.class)
.flatMap(this::authenticateToken)
.map(user ->
new Authorization(user, (AuthHeaders) authentication.getCredentials()));
}
private <T> Mono<T> raiseBadCredentials() {
return Mono.error(new TokenValidationException("Invalid Credentials"));
}
private Mono<User> authenticateToken(Authorization authenticationToken) {
AuthHeaders authHeaders = (AuthHeaders) authenticationToken.getCredentials();
return Optional.of(authHeaders)
.map(headers -> authWebClient.validateUserToken(authHeaders.getAuthToken(), authHeaders.getRequestId())
.doOnSuccess(user -> log
.info("Authenticated user " + user.getUsername() + ", setting security context")))
.orElseThrow(() -> new MissingHeaderException("Authorization is missing"));
}
}
SecurityUtils.java
#Slf4j
public class SecurityUtils {
public static Mono<AuthHeaders> getAuthHeaders() {
return getSecurityContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getCredentials)
.cast(AuthHeaders.class)
.doOnSuccess(authHeaders -> log.info("Auth headers: {}", authHeaders));
}
private static Mono<SecurityContext> getSecurityContext() {
return ReactiveSecurityContextHolder.getContext();
}
}
WebClient building
(...)
WebClient.builder()
.baseUrl(url)
.filter(loggingFilter)
.defaultHeaders(httpHeaders ->
getAuthHeaders()
.doOnNext(headers -> httpHeaders.putAll(
Map.of(
REQUEST_ID, singletonList(headers.getRequestId()),
AUTHORIZATION, singletonList(headers.getAuthToken())))))
.build()
(...)
The main problem is inside a building of my WebClient - I expect, that I'll have complete webclient with requested header, but as I described above - I have an empty context inside SecurityUtils.java
So, after investigating and debugging I found a reason using combining streams into single one. IMHO, this is not quite reason for this issue, but now it works.
(...)
getAuthHeaders()
.flatMap(authHeaders -> buildWebClient(url, authHeaders)
.get()
(...)
Related
I try to create a simple authentication schema with Reactive approach.
I've created a project from scratch with dependencies to reactive components and security.
Introduced Configuration file where I configure authentication manager and security context repository.
The problem is that I notice, that Mono injected into controller initiates double requests to "login" endpoint.
Why does it happens and how to prevent it?
Here is the code of configuration:
#Configuration
#EnableWebFluxSecurity
public class WebFluxSecurityConfiguration {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private SecurityContextRepository securityContextRepository;
#Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http) {
return http
.csrf().disable()
.cors().disable()
.httpBasic().disable()
.logout().disable()
.formLogin().disable()
.authorizeExchange()
.pathMatchers("/login").permitAll()
.anyExchange().authenticated()
.and()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.build();
}
}
Here is the authentication manager
#Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
private final WebClient webClient;
public AuthenticationManager(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder.baseUrl("http://localhost:8080/login")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return webClient.post()
.header("Authorization","Bearer bla-bla")
.retrieve()
.bodyToMono(String.class)
.map(r->new AuthenticatedUser());
}
}
And here is a security context repository
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
private static final String TOKEN_PREFIX = "Bearer ";
private AuthenticationManager authenticationManager;
List<PathPattern> pathPatternList;
public SecurityContextRepository(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
PathPattern pathPattern1 = new PathPatternParser().parse("/login");
pathPatternList = new ArrayList<>();
pathPatternList.add(pathPattern1);
}
#Override
public Mono load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
RequestPath path = request.getPath();
if (pathPatternList.stream().anyMatch(pathPattern -> pathPattern.matches(path.pathWithinApplication()))) {
System.out.println(path.toString() + " path excluded");
return Mono.empty();
}
System.out.println("executing logic for " + path.toString() + " path");
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
String authToken = null;
//test
authHeader = "Bearer bla-bla";
//~test
if (authHeader != null && authHeader.startsWith(TOKEN_PREFIX)) {
authToken = authHeader.replace(TOKEN_PREFIX, "");
}else {
System.out.println("couldn't find bearer string, will ignore the header.");
}
if (authToken != null) {
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return this.authenticationManager.authenticate(auth).map((authentication) -> new SecurityContextImpl(authentication));
} else {
return Mono.empty();
}
}
#Override
public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
return null;
}
}
link to repository of full project
A quick solution to this particular case is to implement caching of WebClient result. So inside of post method to authorization server for retrieval of user profile I just introduced a method cache().
return webClient.post()
.header("Authorization","Bearer bla-bla")
.retrieve()
.bodyToMono(String.class)
.cache()
.map(r->new AuthenticatedUser());
That helped to avoid repeating queries to authorization endpoint during the request.
I'm building a service which is responsible for allowing a user to link external services to their user account.
Authentication of the web app is using a JWT passed in via query string.
I have a Controller that is attempting to use the OAuth2AuthorizedClientManager to initiate the OAuth client workflow to redirect the user to the target external service and authorize the access.
I am struggling to get the Authorization Grant flow to start - with the code below, my OAuth2AuthorizedClient is always null.
I believe I have a disconnect on how to best use the ClientManager.
Controller:
private final OAuth2AuthorizedClientManager authorizedClientManager;
#Autowired
public LinkingController(OAuth2AuthorizedClientManager authorizedClientManager) {
this.authorizedClientManager = authorizedClientManager;
}
#GetMapping("/tenants/{tenantId}/externalsystem/{externalSystemName}/link")
public ModelAndView linking(#PathVariable Long tenantId, #PathVariable String externalSystemName, Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
// ClientRegistrationRepository does not natively support multi-tenancy, so registrations are
// Prefixed with a tenantId
String registrationId = tenantId + "-" + externalSystemName;
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(registrationId)
.principal(authentication) // Confirmed this is the correct authentication from the JWT
.attributes(attrs -> {
attrs.put(HttpServletRequest.class.getName(), servletRequest);
attrs.put(HttpServletResponse.class.getName(), servletResponse);
})
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest); // Confirmed with debugging that it is using the correct registration repository and finding the correct registration.
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
Map<String, Object> params = new HashMap<>();
params.put("token", accessToken.getTokenValue());
return new ModelAndView("rootView", params);
}
Security Config:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.oauth2Client()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.bearerTokenResolver(getCustomizedTokenResolver())
.jwt();
}
private BearerTokenResolver getCustomizedTokenResolver() {
DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
resolver.setAllowFormEncodedBodyParameter(true);
resolver.setAllowUriQueryParameter(true);
return resolver;
}
#Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new BridgeClientRegistrationRepository(externalSystemService);
}
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(OAuth2AuthorizedClientRepository authorizedClientRepository) {
return new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository(), authorizedClientRepository);
}
}
UPDATE
My code originally defined the OAuth2AuthorizedClientManager as:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(OAuth2AuthorizedClientRepository authorizedClientRepository) {
return new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository(), authorizedClientRepository);
}
It seems that without adding specific providers to the ClientProvider, it would not function. Corrected code is:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
I'm trying to setup with Spring webflux security custom string messages, if the login/logout was successfull or it's failed. The handlers are working except the "authenticationFailureHandler".
The docs mention something about "logoutSuccessUrl()" but it does not exist, and these handlers are returning with Mono< Void >.
So I have 2 questions:
How do I return some string as a response, for example if the authenticaion fails then something like "Invalid username or password", or some json string. I tried to redirect to an action, but i can't do it with void Mono.
Why the authenticationFailureHandler is not working, and every other handler does? Is that a bug?
-I tried to redirect with Mono.just("redirect:/some-url").then(), but it didn't do anything. For the responses
I made my handlers bean, tried to change the method sequence, disabling/enabling other handlers. For the authenticationFailureHandler.
You can find my whole code here:
https://github.com/iron2414/WebFluxAuth
It's a modified version of this article's code:
https://medium.com/#mgray_94552/reactive-authorisation-in-spring-security-943e6534aaeb
The security configuration looks like this:
return http
.exceptionHandling()
.accessDeniedHandler((swe, e) -> {
System.out.println("ACCESS DENIED");
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
})
.authenticationEntryPoint((swe, e) -> {
System.out.println("AUTHENTICATION ENTRTY POINT");
ServerHttpResponse response = swe.getResponse();
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
})
.and()
.authorizeExchange()
.pathMatchers("/**").authenticated()
.and()
.formLogin()
.authenticationFailureHandler(MyAuthenticationFailureHandler())
.authenticationSuccessHandler(MyAuthenticationSuccessHandler()).and()
.logout().logoutSuccessHandler(MyLogoutHandler())
.and()
.csrf()
.disable()
.build();
The loginFailure handler:
#Bean
public MyAuthenticationFailureHandler MyAuthenticationFailureHandler() {
return new MyAuthenticationFailureHandler();
}
#Component
public class MyAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
#Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) {
//TODO redirect
System.out.println("AUTHENTICATION FAILURE");
return Mono.empty();
}
}
For the response i had to use "RedirectServerAuthenticationSuccessHandler" for the authentication success. And i created the exact same class that implements for the failure.
The auth failure handler not working is a known issue. In order to make it work you have to set the loginpage. So i just updated it to the default "/login"
I came across the same issue and end up using a custom DefaultErrorWebExceptionHandler to deal with it:
#Component
public class MyGlobalWebExceptionHandler extends DefaultErrorWebExceptionHandler {
#Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return route(all(), this::renderErrorResponse);
}
#Override
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
ErrorRepresentation representation = createErrorRepresentation(request);
return ServerResponse
.status(representation.getError().getStatus())
.contentType(APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(representation));
}
private ErrorRepresentation createErrorRepresentation(ServerRequest request) {
boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL);
Map<String, Object> error = getErrorAttributes(request, includeStackTrace);
Throwable throwable = getError(request);
if (throwable instanceof AuthenticationException) {
AuthenticationFailureException failureException = new AuthenticationFailureException(throwable.getMessage());
return new ErrorRepresentation(failureException, error.get("path").toString());
}
}
}
I'm having trouble setting up a ResourceServer that uses Webflux in spring-boot 2.1.3.RELEASE. The same token used to authenticate is working fine with a resource server that doesn't use Webflux, and if I set .permitAll() it works too (obviously). Here's my resource server config
The authorization server uses jwt token store.
#EnableWebFluxSecurity
public class ResourceServerConfig {
#Value("${security.oauth2.resource.jwt.key-value}")
private String publicKey;
#Autowired Environment env;
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange()
.pathMatchers("/availableInspections/**").hasRole("READ_PRIVILEGE")
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtDecoder(reactiveJwtDecoder())
;
return http.build();
}
#Bean
public ReactiveJwtDecoder reactiveJwtDecoder() throws Exception{
return new NimbusReactiveJwtDecoder(getPublicKeyFromString(publicKey));
}
public static RSAPublicKey getPublicKeyFromString(String key) throws IOException, GeneralSecurityException {
String publicKeyPEM = key;
publicKeyPEM = publicKeyPEM.replace("-----BEGIN PUBLIC KEY-----\n", "");
publicKeyPEM = publicKeyPEM.replace("-----END PUBLIC KEY-----", "");
byte[] encoded = Base64.getMimeDecoder().decode(publicKeyPEM);
KeyFactory kf = KeyFactory.getInstance("RSA");
RSAPublicKey pubKey = (RSAPublicKey) kf.generatePublic(new X509EncodedKeySpec(encoded));
return pubKey;
}
}
The error I'm getting is
WWW-Authenticate: Bearer error="insufficient_scope", error_description="The token provided has insufficient scope [read] for this request", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1", scope="read"
I've verified that the token has the required scope...
What do I need to change/add in order to get my token read correctly?
Spring seems to add only what is under scope in the jwt token, ignoring everything from authorities - so they can't be used in a webflux resource server unless we extend JwtAuthenticationConverter to also add from authorities (or other claims) in the token. In the security config, I've added jwtAuthenticationConverter
http
// path config like above
.oauth2ResourceServer()
.jwt()
.jwtDecoder(reactiveJwtDecoder())
.jwtAuthenticationConverter(converter())
and have overriden JwtAuthenticationConverter to include authorities granted authorities:
public class MyJwtAuthenticationConverter extends JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private static final String SCOPE_AUTHORITY_PREFIX = "SCOPE_";
private static final Collection<String> WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES =
Arrays.asList("scope", "scp", "authorities"); // added authorities
protected Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
return this.getScopes(jwt)
.stream()
.map(authority -> SCOPE_AUTHORITY_PREFIX + authority)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
private Collection<String> getScopes(Jwt jwt) {
Collection<String> authorities = new ArrayList<>();
// add to collection instead of returning early
for ( String attributeName : WELL_KNOWN_SCOPE_ATTRIBUTE_NAMES ) {
Object scopes = jwt.getClaims().get(attributeName);
if (scopes instanceof String) {
if (StringUtils.hasText((String) scopes)) {
authorities.addAll(Arrays.asList(((String) scopes).split(" ")));
}
} else if (scopes instanceof Collection) {
authorities.addAll((Collection<String>) scopes);
}
}
return authorities;
}
}
A little bit late, but a simpler solution could be (in kotlin, but easy to translate to java):
class CustomJwtAuthenticationConverter : Converter<Jwt, AbstractAuthenticationToken> {
private val jwtGrantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
override fun convert(source: Jwt): AbstractAuthenticationToken {
val scopes = jwtGrantedAuthoritiesConverter.convert(source)
val authorities = source.getClaimAsStringList("authorities")?.map { SimpleGrantedAuthority(it) }
return JwtAuthenticationToken(source, scopes.orEmpty() + authorities.orEmpty())
}
}
and then supply implementation to WebSecurityConfigurerAdapter, like:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Configuration
class ResourceServerConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http {
oauth2ResourceServer {
jwt {
jwtDecoder = reactiveJwtDecoder()
jwtAuthenticationConverter = CustomJwtAuthenticationConverter()
}
}
}
}
}
with springboot+webflux in version 2.7.1 and Keycloak as IdP, I have the same issue
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange()
.pathMatchers("/availableInspections/**").hasRole("READ_PRIVILEGE")
.and()
.oauth2ResourceServer()
.jwt()
// .jwtDecoder(reactiveJwtDecoder()) not used
;
return http.build();
}
=> Gives the following error : www-authenticate: Bearer error="insufficient_scope",error_description="The request requires higher privileges than provided by the access token.",error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
Then I see that the line ".anyExchange().authenticated()" was missing on my project :
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange()
.pathMatchers("/availableInspections/**").hasRole("READ_PRIVILEGE")
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
// .jwtDecoder(reactiveJwtDecoder()) not used
;
return http.build();
}
=> no more error :) !
I want to implement simple Spring Security WebFlux application.
I want to use JSON message like
{
'username': 'admin',
'password': 'adminPassword'
}
in body (POST request to /signin) to sign in my app.
What did I do?
I created this configuration
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity(proxyTargetClass = true)
public class WebFluxSecurityConfig {
#Autowired
private ReactiveUserDetailsService userDetailsService;
#Autowired
private ObjectMapper mapper;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(11);
}
#Bean
public ServerSecurityContextRepository securityContextRepository() {
WebSessionServerSecurityContextRepository securityContextRepository =
new WebSessionServerSecurityContextRepository();
securityContextRepository.setSpringSecurityContextAttrName("securityContext");
return securityContextRepository;
}
#Bean
public ReactiveAuthenticationManager authenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager =
new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
authenticationManager.setPasswordEncoder(passwordEncoder());
return authenticationManager;
}
#Bean
public AuthenticationWebFilter authenticationWebFilter() {
AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager());
filter.setSecurityContextRepository(securityContextRepository());
filter.setAuthenticationConverter(jsonBodyAuthenticationConverter());
filter.setRequiresAuthenticationMatcher(
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/signin")
);
return filter;
}
#Bean
public Function<ServerWebExchange, Mono<Authentication>> jsonBodyAuthenticationConverter() {
return exchange -> {
return exchange.getRequest().getBody()
.cache()
.next()
.flatMap(body -> {
byte[] bodyBytes = new byte[body.capacity()];
body.read(bodyBytes);
String bodyString = new String(bodyBytes);
body.readPosition(0);
body.writePosition(0);
body.write(bodyBytes);
try {
UserController.SignInForm signInForm = mapper.readValue(bodyString, UserController.SignInForm.class);
return Mono.just(
new UsernamePasswordAuthenticationToken(
signInForm.getUsername(),
signInForm.getPassword()
)
);
} catch (IOException e) {
return Mono.error(new LangDopeException("Error while parsing credentials"));
}
});
};
}
#Bean
public SecurityWebFilterChain securityWebFiltersOrder(ServerHttpSecurity httpSecurity,
ReactiveAuthenticationManager authenticationManager) {
return httpSecurity
.csrf().disable()
.httpBasic().disable()
.logout().disable()
.formLogin().disable()
.securityContextRepository(securityContextRepository())
.authenticationManager(authenticationManager)
.authorizeExchange()
.anyExchange().permitAll()
.and()
.addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}
}
BUT I use jsonBodyAuthenticationConverter() and it reads Body of the incoming request. Body can be read only once, so I have an error
java.lang.IllegalStateException: Only one connection receive subscriber allowed.
Actually it's working but with exception (session is set in cookies). How can I remake it without this error?
Now I only created something like:
#PostMapping("/signin")
public Mono<Void> signIn(#RequestBody SignInForm signInForm, ServerWebExchange webExchange) {
return Mono.just(signInForm)
.flatMap(form -> {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
form.getUsername(),
form.getPassword()
);
return authenticationManager
.authenticate(token)
.doOnError(err -> {
System.out.println(err.getMessage());
})
.flatMap(authentication -> {
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
return securityContextRepository
.save(webExchange, securityContext)
.subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
});
});
}
And removed AuthenticationWebFilter from config.
You are almost there. The following converter worked for me:
public class LoginJsonAuthConverter implements Function<ServerWebExchange, Mono<Authentication>> {
private final ObjectMapper mapper;
#Override
public Mono<Authentication> apply(ServerWebExchange exchange) {
return exchange.getRequest().getBody()
.next()
.flatMap(buffer -> {
try {
SignInRequest request = mapper.readValue(buffer.asInputStream(), SignInRequest.class);
return Mono.just(request);
} catch (IOException e) {
log.debug("Can't read login request from JSON");
return Mono.error(e);
}
})
.map(request -> new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
}
}
Furthermore, you don't need the sign in controller; spring-security will check each request for you in the filter. Here's how I configured spring-security with an ServerAuthenticationEntryPoint:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ReactiveAuthenticationManager authManager) {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/api/**").authenticated()
.pathMatchers("/**", "/login", "/logout").permitAll()
.and().exceptionHandling().authenticationEntryPoint(restAuthEntryPoint)
.and().addFilterAt(authenticationWebFilter(authManager), SecurityWebFiltersOrder.AUTHENTICATION)
.logout()
.and().build();
}
Hope this helps.
Finally I config WebFlux security so (pay attention to logout handling, logout doesn't have any standard ready-for-use configuration for 5.0.4.RELEASE, you must disable default logout config anyway, because default logout spec creates new SecurityContextRepository by default and doesn't allow you to set your repository).
UPDATE: default logout configuration doesn't work only in case when you set custom SpringSecurityContextAttributeName in SecurityContextRepository for web session.
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity(proxyTargetClass = true)
public class WebFluxSecurityConfig {
#Autowired
private ReactiveUserDetailsService userDetailsService;
#Autowired
private ObjectMapper mapper;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(11);
}
#Bean
public ServerSecurityContextRepository securityContextRepository() {
WebSessionServerSecurityContextRepository securityContextRepository =
new WebSessionServerSecurityContextRepository();
securityContextRepository.setSpringSecurityContextAttrName("langdope-security-context");
return securityContextRepository;
}
#Bean
public ReactiveAuthenticationManager authenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager =
new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
authenticationManager.setPasswordEncoder(passwordEncoder());
return authenticationManager;
}
#Bean
public SecurityWebFilterChain securityWebFiltersOrder(ServerHttpSecurity httpSecurity) {
return httpSecurity
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.securityContextRepository(securityContextRepository())
.authorizeExchange()
.anyExchange().permitAll() // Currently
.and()
.addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterAt(logoutWebFilter(), SecurityWebFiltersOrder.LOGOUT)
.build();
}
private AuthenticationWebFilter authenticationWebFilter() {
AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager());
filter.setSecurityContextRepository(securityContextRepository());
filter.setAuthenticationConverter(jsonBodyAuthenticationConverter());
filter.setAuthenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/home"));
filter.setAuthenticationFailureHandler(
new ServerAuthenticationEntryPointFailureHandler(
new RedirectServerAuthenticationEntryPoint("/authentication-failure")
)
);
filter.setRequiresAuthenticationMatcher(
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/signin")
);
return filter;
}
private LogoutWebFilter logoutWebFilter() {
LogoutWebFilter logoutWebFilter = new LogoutWebFilter();
SecurityContextServerLogoutHandler logoutHandler = new SecurityContextServerLogoutHandler();
logoutHandler.setSecurityContextRepository(securityContextRepository());
RedirectServerLogoutSuccessHandler logoutSuccessHandler = new RedirectServerLogoutSuccessHandler();
logoutSuccessHandler.setLogoutSuccessUrl(URI.create("/"));
logoutWebFilter.setLogoutHandler(logoutHandler);
logoutWebFilter.setLogoutSuccessHandler(logoutSuccessHandler);
logoutWebFilter.setRequiresLogoutMatcher(
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout")
);
return logoutWebFilter;
}
private Function<ServerWebExchange, Mono<Authentication>> jsonBodyAuthenticationConverter() {
return exchange -> exchange
.getRequest()
.getBody()
.next()
.flatMap(body -> {
try {
UserController.SignInForm signInForm =
mapper.readValue(body.asInputStream(), UserController.SignInForm.class);
return Mono.just(
new UsernamePasswordAuthenticationToken(
signInForm.getUsername(),
signInForm.getPassword()
)
);
} catch (IOException e) {
return Mono.error(new LangDopeException("Error while parsing credentials"));
}
});
}
}