Setting up Swagger UI with Spring WebFlux - spring

I am currently in the process of setting up a Swagger UI interface for one of the projects I am working on and I am experiencing various issues.
My project uses Spring security to secure the API calls using bearer token authentication, so I need to provide a way of enabling the input dialog so that users can input their bearer token. I have tried everything mentioned in the documentation of OpenAPI regarding this but nothing seems to work in rendering the dialog correctly.
Secondly the project does CSRF checks and even though my application properties include springdoc.swagger-ui.csrf.enabled=true the check fails constantly. I have a dead end and I have no idea how to resolve both problems. For reference my security configuration is the following:
#Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity security) {
if (securityProperties.isEnabled()) {
return security
.securityMatcher(new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers(securityProperties.getIgnoredPaths())))
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(entryPoint)
.and()
.cors()
.and()
.authorizeExchange(spec -> spec.anyExchange().authenticated())
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt)
.build();
}
return security
.securityMatcher(new PathPatternParserServerWebExchangeMatcher("/**"))
.authorizeExchange(spec -> spec.anyExchange().permitAll())
.csrf()
.disable()
.build();
}

We fixed it with our multi-provider (OAuth2 Keycloak for API and Basic Auth for Swagger UI) Webflux security configuration by adding this to every application.yaml:
springdoc:
api-docs:
enabled: true
swagger-ui:
oauth:
client-id: dev
client-secret: 123
scopes: [openid]
csrf:
enabled: false
Key point here is csrf.enabled: false.
Our Keycloak security configuration:
// Keycloak-based JWT authorization for #RestControllers
#Order(1)
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class JwtSecurityConfig {
#Bean("jwt")
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange()
.pathMatchers("/api/**")
.authenticated()
.and()
.csrf()
.disable()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(grantedAuthoritiesExtractor());
return http.build();
}
private Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>>
grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new GrantedAuthoritiesExtractor());
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
}

Related

How combine two filter chains with spring security (jwt, basic auth)?

currently I have a security configuration which works fine. However I would like to optimize it.
The application defines an JwtFilter which checks requests for a token in the http header or cookie, if there is one its checked.
Now, for endpoints like actuator/metrics or swagger I defined a second filterchain with a securityMatcher (after spring boot 3 migration) to allow basic auth for those paths.
#Bean
#Order(62)
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests {
it.requestMatchers("/auth/**").permitAll()
it.requestMatchers(HttpMethod.GET, "/test").permitAll()
it.anyRequest().authenticated()
}
.addFilterBefore(
JwtFilter(jwtTokenService),
BasicAuthenticationFilter::class.java
)
.addFilterBefore(FilterChainExceptionHandler(handlerExceptionResolver), JwtFilter::class.java)
.exceptionHandling().authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
return http.build()
}
#Bean
#Order(1)
fun specialPaths(http: HttpSecurity): SecurityFilterChain {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.securityMatcher("/actuator/**", "/v3/api-docs/**", "/swagger/**")
.authorizeHttpRequests {
it.requestMatchers("/actuator/**").hasAnyRole("ADMIN")
it.requestMatchers("/v3/api-docs/**").hasAnyRole("ADMIN")
it.requestMatchers("/swagger/**").hasAnyRole("ADMIN")
}
return http.build()
}
I tried merging the configs in lots of ways, however it never works out like with the two separate chains.
Any tips or hints are greatly appreciated.

Spring Security Context Authentication is null

i am trying to add couple of filters in my request processing in spring boot security config.
Below is my code
#EnableWebSecurity
#Configuration
public class JwtSecurityConfiguration {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(this::configureEndpoints)
return http.build();
}
private void configureEndpoints(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry authorizationManagerRequestMatcherRegistry){
authorizationManagerRequestMatcherRegistry.mvcMatchers("/permit")
.permitAll()
.mvcMatchers("/block")
.denyAll()
.and()
.mvcMatcher("/api")
.addFilterBefore(new Filter1(), SecurityContextHolderAwareRequestFilter.class)
// register TenantFilter in the chain after the SecurityContext is made available by the respective filter
.mvcMatcher("/api")
.addFilterAfter(new Filter2(), SecurityContextHolderAwareRequestFilter.class)
.authorizeHttpRequests()
.mvcMatchers("/api")
.authenticated()
.and();
}
}
It seems the authentication does not happen and filters are never hit.
If i try to access the authentication in my runtime code i get SecurityContextHolder.getContext().getAuthentication() as null.
Seems to some problem in the security configuration only.

Spring Boot 3.0 + Security 6 +WebFlux causes "An expected CSRF token cannot be found" in Postman

The following SecurityWebFilterChain works very fine in Spring Boot 2.7.x but not working any more in Spring Boot 3.0.0. It just show "An expected CSRF token cannot be found" when calling the REST API in Postman. Would you please to teach me how to solve it?
#Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
http
.cors().disable()
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint((swe, e) ->
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))
).accessDeniedHandler((swe, e) ->
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN))
)
.and()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange(exchange -> exchange
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers("/login", "/register").permitAll()
.anyExchange().authenticated()
.and()
.cors().disable()
.csrf().disable()
)
.formLogin().disable()
.httpBasic().disable()
;
return http.csrf(csrf -> csrf.disable()).build();
}
I experienced the same symptoms when migrating my webflux application to Spring Boot 3.0.0 today, which worked perfectly with 2.7.5. So I googled for "csrf-disabling no longer working" and found this and some few other posts...
However in my case, it was an annotation change of Spring security 6, that caused the problem: #EnableWebFluxSecurity contained "#Configuration" in 5.x version (I checked) - but obviously does no longer and has to be added explicitly.
Thus the complete SecurityWebFilterChain bean was not found after migrating... Now the working code looks as follows:
#EnableWebFluxSecurity
#Configuration // <- this was integrated in #EnableWebFluxSecurity with Spring Security 5.x
public class AccountWebSecurityConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ReactiveAuthenticationManager authenticationManager,
ServerAccessDeniedHandler accessDeniedHandler,
ServerAuthenticationEntryPoint authenticationEntryPoint) {
http.csrf().disable()
.httpBasic(httpBasicSpec -> httpBasicSpec
.authenticationManager(authenticationManager)
// when moving next line to exceptionHandlingSpecs, get empty body 401 for authentication failures (e.g. Invalid Credentials)
.authenticationEntryPoint(authenticationEntryPoint)
)
.authorizeExchange()
//...
}
As your FilterChain - snippet does not show the annotations at your class, chances are, you may also missing the #Configuration
In my case now everything works as before :-)
You can try it out
https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html
application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
You forgot to enable resource-server in your filter-chain:
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter)
You need a custom authentication converter, as done above, mostly for roles mapping.
Detailed tutorials (for servlets) here: https://github.com/ch4mpy/spring-addons/tree/master/samples/tutorials. You might then refer to samples for reactive apps.

Spring Security not working on any endpoint with multiple WebSecurityConfigurerAdapter implementations

I'm trying to setup Spring Security in my application, which has 3 components:
REST API (under v1 path)
Spring Admin & actuator (under /admin path)
Docs (under /docs and /swagger-ui paths)
I want to setup security like this:
REST API secured with JWT token
Admin secured with HTTP basic
Docs unsecured (public resource)
I've tried to configure authentication for those 3 parts in separate implementations of WebSecurityConfigurerAdapter, and the result looks like this:
For REST API:
#Configuration
#EnableWebSecurity
#Order(1)
class ApiWebSecurityConfig : WebSecurityConfigurerAdapter() {
// FIXME: Temporary override to disable auth
public override fun configure(http: HttpSecurity) {
http
.antMatcher("/v1/*")
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.anyRequest().permitAll()
.and()
.csrf().disable()
}
}
For Spring Admin:
#Configuration
#EnableWebSecurity
#Order(2)
class AdminWebSecurityConfig(
private val adminServerProperties: AdminServerProperties
) : WebSecurityConfigurerAdapter() {
public override fun configure(http: HttpSecurity) {
http.antMatcher("${adminServerProperties.contextPath}/**")
.authorizeRequests()
.antMatchers("${adminServerProperties.contextPath}/assets/**").permitAll()
.antMatchers("${adminServerProperties.contextPath}/login").permitAll()
.anyRequest()
.authenticated()
.and()
.cors()
.and()
.formLogin()
.loginPage("${adminServerProperties.contextPath}/login")
.and()
.logout()
.logoutUrl("${adminServerProperties.contextPath}/logout")
.and()
.httpBasic()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers(
AntPathRequestMatcher("${adminServerProperties.contextPath}/instances", HttpMethod.POST.toString()),
AntPathRequestMatcher("${adminServerProperties.contextPath}/instances/*", HttpMethod.DELETE.toString()),
AntPathRequestMatcher("${adminServerProperties.contextPath}/actuator/**")
)
}
#Bean
fun corsConfigurationSource(): CorsConfigurationSource = UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", CorsConfiguration().apply {
allowedOrigins = listOf("*")
allowedMethods = listOf("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH")
allowCredentials = true
allowedHeaders = listOf("Authorization", "Cache-Control", "Content-Type")
})
}
}
And for public docs:
#Configuration
#EnableWebSecurity
#Order(3)
class DocsWebSecurityConfig : WebSecurityConfigurerAdapter() {
public override fun configure(http: HttpSecurity) {
http
.requestMatchers()
.antMatchers("/swagger-ui/**", "/docs/**", "/docs-oas3/**")
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
}
}
And my main application class looks like this:
#SpringBootApplication
#EnableAdminServer
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#EnableConfigurationProperties(FirebaseConfigurationProperties::class, JwtConfigurationProperties::class)
class HldpDeviceManagementApplication
fun main(args: Array<String>) {
runApplication<HldpDeviceManagementApplication>(*args)
}
When I run the application, there's no error or any security information, besides this log output:
Will not secure Ant [pattern='/v1/**']
Will not secure Ant [pattern='/admin/**']
Will not secure Or [Ant [pattern='/swagger-ui/**'], Ant [pattern='/docs/**'], Ant [pattern='/docs-oas3/**']]
Any suggestion why doesn't the configuration work? Or maybe another way I can secure the application like this? I've tried doing a few changes in the configuration, but nothing seems to help.
I've found the problem - it's a bug in the latest version of Spring:
https://github.com/spring-projects/spring-security/issues/10909
You didn't mention when it doesn't work, is it when you make a request, or on application startup? However, I can help you with your configuration and get the information needed to solve the problem.
I'll try to simplify your configuration with the new way to configure HttpSecurity, by exposing a SecurityFilterChain bean.
#Configuration
public class SecurityConfiguration {
#Bean
#Order(0)
public SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.requestMatchers(requests -> requests.antMatchers("/v1/**"))
...
// the rest of the configuration
return http.build();
}
#Bean
#Order(1)
public SecurityFilterChain admin(HttpSecurity http) throws Exception {
http
.requestMatchers(requests -> requests.antMatchers("/admin/**"))
...
// the rest of the configuration
return http.build();
}
#Bean
#Order(2)
public SecurityFilterChain docs(HttpSecurity http) throws Exception {
http
.requestMatchers(requests -> requests.antMatchers("/docs/**"))
...
// the rest of the configuration
return http.build();
}
}
This is in Java, but you can adapt to Kotlin easily, I'm sorry to not provide it in Kotlin already. With this simplified configuration, now you can add logging.level.org.springframework.security=TRACE to your application.properties file and check what Spring Security is doing by reading the logs.

Spring webflux security - Form based auth testing using postman

I am using Spring boot for backend and reactjs for front and both are running as different application with context switching on the load balancer so as to have the same app URL for both.
Context Switching:-
All requests routed to reach JS by default
Request starts with /api/v1/* will be routed to Spring boot backend.
Using Spring webflux security for the Spring boot application.
#Bean
public SecurityWebFilterChain securitygWebFilterChain(final ServerHttpSecurity http) {
http.securityContextRepository(securityContextRepository);
return http.authorizeExchange()
.matchers(PathRequest.toStaticResources().atCommonLocations())
.permitAll()
.pathMatchers(props.getSecurity().getIgnorePatterns())
.permitAll()
.anyExchange()
.authenticated()
.and()
.formLogin()
.and()
.exceptionHandling()
.authenticationEntryPoint((exchange, exception) -> Mono.error(exception))
.accessDeniedHandler((exchange, exception) -> Mono.error(exception))
.and()
.build();
}
#Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
#Bean
public ReactiveAuthenticationManager authenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(
userDetailsService);
authenticationManager.setPasswordEncoder(passwordEncoder());
return authenticationManager;
}
#Bean
public ServerSecurityContextRepository securityContextRepository() {
WebSessionServerSecurityContextRepository securityContextRepository = new WebSessionServerSecurityContextRepository();
securityContextRepository.setSpringSecurityContextAttrName("app-security-context");
return securityContextRepository;
}
Using form based authentication here. All API's are restricted by default.
Need to test the login and API's with proper authentication. My react app is not yet ready so trying to use Postman for testing.
How to test the form login with postman?
Also, How to pass the login session for testing the other authenticated API's?

Resources