I want to create a BFF micro-service, which would be responsible for (among other things) forwarding username/password credentials to Keycloak (where Keycloak issues a JWT token) and for validating said tokens with Keycloak on protected endpoints.
I'm having some issues exposing public endpoints, however. I want to expose an endpoint, which does not need JWT security (since it's the endpoint where the user will POST their credentials to exchange for a token), but can't get it to work. I want to expose the endpoint api/v1/backoffice/auth
Can someone guide me how this is done?
Security config:
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain globalSecurityWebFilterChain(ServerHttpSecurity http) {
return http
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.logout(ServerHttpSecurity.LogoutSpec::disable)
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange(exchanges -> exchanges.anyExchange().authenticated())
.oauth2ResourceServer(oAuth -> oAuth.jwt(Customizer.withDefaults()))
.build();
}
}
Dependencies:
dependencies {
// Intra-project dependencies
implementation(project(":common"))
// Security dependencies
implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.1")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.4.1")
implementation("org.springframework.boot:spring-boot-starter-security:3.4.1")
implementation("org.springframework.boot:spring-boot-starter-webflux:3.4.1")
// Dev dependencies
developmentOnly("org.springframework.boot:spring-boot-devtools:3.4.1")
// Communication
implementation("org.springframework.boot:spring-boot-starter-amqp:3.4.1")
implementation("org.springframework.amqp:spring-rabbit-stream:3.2.1")
// Cloud capacity
implementation("org.springframework.cloud:spring-cloud-starter-config:4.2.0")
implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:4.2.0") {
exclude(group = "com.fasterxml.woodstox", module = "woodstox-core")
exclude(group = "org.apache.httpcomponents", module = "httpclient")
exclude(group = "com.thoughtworks.xstream", module = "xstream")
}
// resolve transitive dependency security issues
implementation("com.fasterxml.woodstox:woodstox-core:7.1.0")
implementation("org.apache.httpcomponents:httpclient:4.5.14")
implementation("com.thoughtworks.xstream:xstream:1.4.21")
}
Controller to handle the call (haven't tested it at all)
@RestController
@RequestMapping("/api/v1/backoffice")
public class BoAuthController {
@Value("${keycloak.client.secret}")
private String clientSecret;
private final WebClient webClient = WebClient.create("http://localhost:8080");
@PostMapping("/auth")
public Mono<ResponseEntity<Map>> login(@RequestBody Map<String, String> credentials) {
return webClient.post()
.uri("/realms/backoffice-realm/protocol/openid-connect/token")
.header("Content-Type", "application/x-www-form-urlencoded")
.bodyValue("grant_type=password&client_id=backoffice-client&client_secret=" + clientSecret
+ "&username=" + credentials.get("username")
+ "&password=" + credentials.get("password"))
.retrieve()
.bodyToMono(Map.class)
.map(ResponseEntity::ok)
.onErrorResume(error -> Mono.just(
ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Invalid credentials"))
));
}
}
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http,
CustomUserDetailsService customUserDetailsService,
JwtAuthFilter jwtAuthFilter) {
return http.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchanges -> exchanges
.pathMatchers(
"/swagger-ui/**",
"/v3/api-docs/**",
"/api/v1/auth/**"
)
.permitAll()
.anyExchange()
.authenticated()
)
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) // Stateless session
.headers(headers -> headers
.frameOptions(ServerHttpSecurity.HeaderSpec.FrameOptionsSpec::disable)) // Allow H2 console
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint((exchange, e) ->
Mono.fromRunnable(() -> exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))))
.addFilterAt(jwtAuthFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}