I'm implementing a simple Sign-In via Google Oauth2 for my Spring Webflux backend and ReactJS frontend. For now I'm just trying to get the API working and test it with an OpenAPI swagger page.
I've almost got the sign-in working, except for one problem - no X-CSRF cookie is being attached to my API's response after logging into Google - it's only attaching a SESSION cookie. So GET requests to secured endpoints are working fine, because that SESSION cookie links to a valid session with the user details, however POST requests are failing because there is no JS-accessible X-CSRF cookie to attach as a header.
Specifically, the request to http://localhost:8080/login/oauth2/code/google?state=...&code=...&scope=email+profile+......
responds with a 302 status and headers:
Location: /swagger-ui.html
to bring me back to the swagger UI pageSet-Cookie: SESSION=<session-id>
because I am now logged inBut that's it, there's no Set-Cookie header for the X-CSRF token.
Confusingly, the X-CSRF token IS being attached via Set-Cookie when I go to Spring's /logout
page... But that's pretty useless, I want the X-CSRF cookie attached in the same response that sets the SESSION cookie in the first place. Because at that point the User has logged in, so I want my Spring API to dish them out a X-CSRF cookie alongside the SESSION cookie.
What am I missing here in my config?
EDIT: To be clear, my swagger UI is working just fine once the X-CSRF cookie is returned to my browser. As long as I first go to the Spring /logout
page, which responds with a Set-Cookie header with the X-CSRF cookie, if I go back to the Swagger UI - it will then use that cookie correctly by attaching it in a matching header.
My question is - how do I get this X-CSRF cookie attached to the same response that sets the SESSION cookie in the login process?
My SecurityConfig.kt:
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http
.csrf { csrf -> csrf
.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(ServerCsrfTokenRequestAttributeHandler())
}
.authorizeExchange { exchange ->
exchange
.anyExchange()
.authenticated()
}
.oauth2Login(Customizer.withDefaults())
.build()
}
}
My application.yml:
springdoc:
swagger-ui:
enabled: true
csrf:
enabled: true
header-name: "X-XSRF-TOKEN"
cookie-name: "XSRF-TOKEN"
url: /my-open-api-spec.yaml
path: /swagger-ui.html
api-docs:
enabled: true
path: /api-docs
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${google_client_id}
client-secret: ${google_client_secret}
scope:
- email
- profile
My Application.kt:
@SpringBootApplication
@EnableR2dbcAuditing
class MyApplication
fun main(args: Array<String>) {
runApplication<MyApplication>(*args)
}
My HttpRouter.kt:
@Configuration
class HttpRouter(val userHandler: UserHandler) {
@Bean
fun routes(): RouterFunction<ServerResponse> {
return router {
"/users".nest {
GET("/me", userHandler::getMyUserDetails)
POST("", userHandler::inviteUser)
GET("", userHandler::searchUsers)
}
}
}
}
You might need to explicitly subscribe to the CSRF cookie by exposing a csrfCookieWebFilter
bean as described there: https://docs.spring.io/spring-security/reference/5.8/migration/reactive.html#_i_am_using_angularjs_or_another_javascript_framework
@Bean
WebFilter csrfCookieWebFilter() {
return (exchange, chain) -> {
Mono<CsrfToken> csrfToken = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
return csrfToken.doOnSuccess(token -> {
/* Ensures the token is subscribed to. */
}).then(chain.filter(exchange));
};
}