Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSRF example for Single-Page Apps could be improved #15105

Closed
jarekkar opened this issue May 20, 2024 · 4 comments
Closed

CSRF example for Single-Page Apps could be improved #15105

jarekkar opened this issue May 20, 2024 · 4 comments
Assignees
Labels
in: docs An issue in Documentation or samples type: enhancement A general enhancement
Milestone

Comments

@jarekkar
Copy link

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: #14149 (comment), 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?

@jarekkar jarekkar added status: waiting-for-triage An issue we've not yet triaged type: enhancement A general enhancement labels May 20, 2024
@sjohnr sjohnr self-assigned this May 22, 2024
@sjohnr
Copy link
Member

sjohnr commented May 22, 2024

@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 sjohnr added status: waiting-for-feedback We need additional information before we can continue in: docs An issue in Documentation or samples and removed status: waiting-for-triage An issue we've not yet triaged labels May 22, 2024
@kcsurapaneni
Copy link
Contributor

@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.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 22, 2024
@jarekkar
Copy link
Author

@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:

  1. To login browser is making GET http://localhost:8080/oauth2/authorization/app
  2. Redirect to IDP
  3. User consent
  4. Redirect back to http://localhost:8080/login/oauth2/code/app, response 302, location: https://my-domain/home
  5. Redirect to https://my-domain/home

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.

@sjohnr
Copy link
Member

sjohnr commented May 23, 2024

@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.

@sjohnr sjohnr changed the title CSRF protection & SPA & OAuth2Login CSRF example for Single-Page Apps could be improved May 23, 2024
@sjohnr sjohnr moved this to Prioritized in Spring Security Team May 28, 2024
@sjohnr sjohnr moved this from Prioritized to In Progress in Spring Security Team May 28, 2024
@sjohnr sjohnr closed this as completed in ee9f5a2 May 29, 2024
@sjohnr sjohnr moved this from In Progress to Done in Spring Security Team May 29, 2024
@sjohnr sjohnr added this to the 6.4.0-M1 milestone May 29, 2024
@sjohnr sjohnr removed the status: feedback-provided Feedback has been provided label May 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: docs An issue in Documentation or samples type: enhancement A general enhancement
Projects
Status: Done
Development

No branches or pull requests

4 participants