Closed jarekkar closed 1 month ago
@jarekkar thank you for providing feedback on the current documentation so that we can improve it!
The solution described in the documentation does save the XSRF-TOKEN cookie after authentication. I have tried several approaches on my own, but they did not work consistently. I found a solution in this comment: #14149 (comment), and it works. However, I am unsure if this is the recommended approach.
The sample code in the comment is a variation on the code provided in the docs. In order to proceed and find the best enhancement to the docs, can you please describe in more detail the issues you encountered following the current recommendation in the docs?
Could you please provide an official description in the documentation (and as a response to that issue) on how to properly adjust the described solution to work well with OAuth2Login?
I am not aware of any specific issues with the example working with OAuth2 Login. Can you please clarify what issues you encountered or provide a sample that demonstrates?
@sjohnr @jarekkar I'd like to highlight that there's an issue with the CSRF protection for Single Page Applications (SPA) official documentation, as outlined in this issue. Exercise care before using/implementing it.
@sjohnr Here is my setup:
@Configuration(proxyBeanMethods = false)
class SecurityConfig {
@Order(1)
@Bean
SecurityFilterChain authenticatedFilterChain(
HttpSecurity http,
ClientRegistrationRepository clientRegistrationRepository
) throws Exception {
String[] all = {
"/oauth2/authorization/app",
"/login/oauth2/code/app",
"/account"
};
DefaultOAuth2AuthorizationRequestResolver resolver = new DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository,
DEFAULT_AUTHORIZATION_REQUEST_BASE_URI
);
resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
http
.securityMatcher(all)
.csrf(csrf -> {
csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
csrf.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler());
})
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.sessionManagement(session -> {
session.sessionCreationPolicy(ALWAYS);
})
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2Login(oauth2 -> {
oauth2.authorizationEndpoint(endpoint -> {
endpoint.authorizationRequestResolver(resolver);
endpoint.authorizationRequestRepository(new HttpSessionOAuth2AuthorizationRequestRepository());
});
oauth2.successHandler(new SimpleUrlAuthenticationSuccessHandler("https://my-domain/home"));
})
.exceptionHandling(
exception -> exception.authenticationEntryPoint(new HttpStatusEntryPoint(UNAUTHORIZED))
);
return http.build();
}
final class CsrfCookieFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
csrfToken.getToken();
filterChain.doFilter(request, response);
}
}
}
public final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
this.delegate.handle(request, response, csrfToken);
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
return super.resolveCsrfTokenValue(request, csrfToken);
}
return this.delegate.resolveCsrfTokenValue(request, csrfToken);
}
}
spring:
security:
oauth2:
client:
provider:
idp:
issuer-uri: ${app.idp.issuer.uri}
user-name-attribute: ${app.idp.claims.user-id}
registration:
app:
client-id: ${app.idp.client.id}
client-secret: ${app.idp.client.secret}
authorization-grant-type: authorization_code
scope: ${app.idp.scope}
client-authentication-method: client_secret_post
provider: idp
Key urls:
Flow:
I would expect that service (in response in step 4.) stores XSRF-TOKEN cookie in the browser, so next request, e.g. POST /accounts can be protected with CSRF token.
Unfortunately cookie is not stored so I started digging on how to enable such behaviour.
I tested it with EntraID (Azure AD) and Okta.
@kcsurapaneni
I'd like to highlight that there's an issue with the CSRF protection for Single Page Applications (SPA) official documentation, as outlined in this issue. Exercise care before using/implementing it.
That issue was just closed as invalid. Please see this comment.
@jarekkar,
Here is my setup
Thanks for providing additional details including your configuration! I now believe I understand the issue you're having.
The documentation example recommends providing a Filter
to read the value of the deferred token (DeferredCsrfToken
), triggering the cookie to be written to the response. This only occurs when the filter is executed. In your configuration, you use SimpleUrlAuthenticationSuccessHandler
to redirect to an external site from the OAuth2LoginAuthenticationFilter
prior to the custom filter being executed.
In the case of an external redirect, you would first want to allow the entire filter chain to be executed. This can be achieved by redirecting to a Spring MVC @Controller
, for example under GET /app
(or any other URL you choose) which then performs a redirect to your frontend app. This would allow the filter provided in the example to cause the cookie to be written prior to the redirect to your frontend app.
There are numerous other ways to handle this situation, depending on preference and other considerations, but it's difficult to anticipate or capture all of those in reference documentation. I worry that covering too many specific scenarios involving frontend view technologies will overwhelm readers. Using a Filter
is not the only way to achieve the goal the docs aim to provide for, but I had hoped it made abundantly clear the distinct elements of the solution for SPAs. Given gh-14149 aims to improve the configuration to hopefully be more self-contained, perhaps we should adjust the documentation example to do something similar without the use of a servlet filter.
If you don't mind, I'd like to repurpose this ticket to address that in the documentation.
Expected Behavior
Please provide a description in the documentation on how to properly set up CSRF protection with SPA and OAuth2Login.
Current Behavior
The current documentation (version 6.2.4) provides a description for BasicAuthentication: https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa
Context
The solution described in the documentation does save the XSRF-TOKEN cookie after authentication. I have tried several approaches on my own, but they did not work consistently. I found a solution in this comment: https://github.com/spring-projects/spring-security/issues/14149#issuecomment-1813453080, and it works. However, I am unsure if this is the recommended approach.
Could you please provide an official description in the documentation (and as a response to that issue) on how to properly adjust the described solution to work well with OAuth2Login?