spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.5k stars 5.78k forks source link

Spring Security 6.x / Single Page Web Application / CSRF - formLogin not working anymore #13011

Closed jornfranke closed 1 year ago

jornfranke commented 1 year ago

Describe the bug I have a Spring Boot 3.x application (with Spring Security 6.x). The frontend is Angular 15.2.x. I am following the instructions here to enable CSFR as well as allow post requests from Angular.

While this works, it has the issue if I use the default Spring Security Configuration in Spring Boot (form login) then after successful login I am logged in to the same Login page - ie I cannot access the application.

To Reproduce Create a simple new Spring Boot 3.x application. Configure Security according to here:

[..]
CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
    XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();

    // set the name of the attribute the CsrfToken will be populated on
    delegate.setCsrfRequestAttributeName("_csrf");

    // Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
    // default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
    CsrfTokenRequestHandler requestHandler = delegate::handle;

    http.authorizeHttpRequests().anyRequest().authenticated().and().formLogin().and().httpBasic().and()
        .csrf((csrf) -> csrf
            .csrfTokenRepository(tokenRepository)
            .csrfTokenRequestHandler(requestHandler)

                );

        return http.build();
[..]

If you do gradlew bootRun and you try to login then after successful login you are redirected to the login page again. If I do NOT have the additional configuration with CSRF then I can login normally (but then I have the issue to do Post requests in Angular).

Edit: Apparently the /login results in an invalid CSRF token foudn issue when submitting the credentials

Expected behavior After successful login it redirects to my application and not back to /login.

jornfranke commented 1 year ago

A workaround causing new issues (one has to refresh the page again).


    http.authorizeHttpRequests().anyRequest().authenticated().and().formLogin().and().httpBasic().and()
        .csrf((csrf) -> csrf
            .csrfTokenRepository(tokenRepository)
            .csrfTokenRequestHandler(requestHandler)
            .ignoreRequestMatchers("/login")
                );

However, then I have to refresh the page after successful login to get a CSRF token... And it has of course also security issues.. Any ideas?

jornfranke commented 1 year ago

If I follow these steps: https://docs.spring.io/spring-security/reference/5.8/migration/servlet/exploits.html#servlet-defer-loading-csrf-token-opt-out then it works. However, does it mean with formLogin one cannot protect against BREACH?

jornfranke commented 1 year ago

I think I have it now with SAML2: Opt in BREACH works + combine it with only this part of opt-out

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
    XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
    // set the name of the attribute the CsrfToken will be populated on
    delegate.setCsrfRequestAttributeName("_csrf");
    // Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
    // default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
    CsrfTokenRequestHandler requestHandler = delegate::handle;
    http
        // ...
        .csrf((csrf) -> csrf
            .csrfTokenRepository(tokenRepository)
            .csrfTokenRequestHandler(requestHandler)
        ).addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);

    return http.build();
}

private static final class CsrfCookieFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        // Render the token value to a cookie by causing the deferred token to be loaded
        csrfToken.getToken();

        filterChain.doFilter(request, response);
    }

}

Still have to check if this works also with formLogin. I had to wrap a bit my head around the protection migration options. The documentation is good, but one has to read it a bit, think that both can be combined and try out a bit.

sjohnr commented 1 year ago

Hi @jornfranke! I'm sorry you've struggled to get your configuration working with the CSRF changes in Spring Security 6. We're working on improving our documentation, and I'd like to understand your thread here to see if it can help inform some of our improvements.

If I follow these steps: https://docs.spring.io/spring-security/reference/5.8/migration/servlet/exploits.html#servlet-defer-loading-csrf-token-opt-out then it works. However, does it mean with formLogin one cannot protect against BREACH?

I may not be following your comments fully in context. Are you asking this in the context of using Form Login + Angular? In other words, are you attempting to support both server-side rendered form login AND an Angular login form?

Still have to check if this works also with formLogin. I had to wrap a bit my head around the protection migration options. The documentation is good, but one has to read it a bit, think that both can be combined and try out a bit.

To clarify, are you suggesting that the documentation could provide an example that combines Form Login AND Angular support? Do you find that the existing suggestion to allow submission of raw CSRF tokens (for Angular applications) is confusing?

jornfranke commented 1 year ago

Hi, thanks for your answer. I admit my description is very confusing as I tried to understand what my issue was :)

On the documentation: Yes I have a SinglePageApplication (SPA) using Angular and Spring Boot 3.x. I managed to get CSRF with SAML2 working. I opted OUT "Defer Loading" and opted IN for CSRF breach.

What confused me here is that you can actually combine both. I managed to do this, but it could be useful to have an example for it for others. Then, what else could be useful is to have concrete use cases, e.g. "If you have a SPA and SAML2 then you need to opt OUT "Defer Loading" and opt IN for CSRF breach.

Do not misunderstand me - the documentation is not bad. I just had to re-read it to get the point.

On the formlogin: I use formLogin only for local development ("localhost") as I do not have SAML2 for local development. This works fine, but I did not manage to get formLogin working with OPT in for CSRF breach. Since it is for local development it is not super critical as in a real deployment SAML2 is used, but I just noticed it. I will provide a more detailed description on this later as I do not have it on this machine.

Thanks again a lot.

sjohnr commented 1 year ago

Thanks for clarifying @jornfranke!

What confused me here is that you can actually combine both. I managed to do this, but it could be useful to have an example for it for others.

This is helpful feedback. I know we did not provide concrete guidance around combining both configurations in the hopes that it would be worked out by the reader, but hearing that it is confusing makes it clear that the documentation could be improved still further.

One question on this point:

Then, what else could be useful is to have concrete use cases, e.g. "If you have a SPA and SAML2 then you need to opt OUT "Defer Loading" and opt IN for CSRF breach.

Do you know for what specific reason you needed to opt out of deferred CSRF tokens? I'm not familiar with how a SPA would interact with a SAML2 protected application, or specifically how CSRF and SAML2 are related here. If possible, can you elaborate on this a bit? It will help me better keep this case in mind when improving the docs.

jornfranke commented 1 year ago

Then, what else could be useful is to have concrete use cases, e.g. "If you have a SPA and SAML2 then you need to opt OUT "Defer Loading" and opt IN for CSRF breach.

Do you know for what specific reason you needed to opt out of deferred CSRF tokens? I'm not familiar with how a SPA would interact with a SAML2 protected application, or specifically how CSRF and SAML2 are related here. If possible, can you elaborate on this a bit? It will help me better keep this case in mind when improving the docs.

I think this is not related to SPA, but to SAML2. For some reason the first post after getting redirected back from the SAML2 IDP to the application fails. One can do two alternative workarounds: Do a dummy post request that fails, but all subsequent are successful or opt out defer loading. Both are not so nice.

sjohnr commented 1 year ago

I think this is not related to SPA, but to SAML2. For some reason the first post after getting redirected back from the SAML2 IDP to the application fails.

Ok, I see. Actually, I believe it's not related to SAML2 and is indeed related to the SPA, because you have to have a way of triggering a new CSRF token to be loaded. This is because the CsrfAuthenticationStrategy causes the old one to be cleared on authentication success, and no new cookie value is written to the response automatically. There are a couple of different ways to handle it, and any of them would probably work, it all depends on how you want to solve it.

This is also helpful to think through, because it relates to the other concepts in this documentation section. As you mentioned, all the information is there in the migration guide, but it's not explained probably in a way that makes it obvious on first reading. We also want to bring these concepts into the main CSRF documentation chapter as well, which I'm starting to work on now.

Anyway, thanks for talking through it!

sjohnr commented 1 year ago

@jornfranke, I have updated the main CSRF chapter of the reference documentation for the 6.1 release. You can preview it here: https://docs.spring.io/spring-security/reference/6.1-SNAPSHOT/servlet/exploits/csrf.html

Your feedback is most welcome on the updates, and we can keep improving.

In the meantime, I'm going mark as a duplicate of gh-13089 and close this issue for now, as I believe there is no additional work to be done with the docs at this time. If there's something you feel I've missed in the update, or you have specific suggestions for the 5.8 migration guide, let me know and we can re-open or open a new specific issue for it. Thanks!

jornfranke commented 1 year ago

wow this looks great and a lot of effort - it looks very good now! Thanks a lot. I will look deeper into it, but at first glance excellent work

sjohnr commented 1 year ago

Thank you very much, @jornfranke!

EtienneKaiser commented 8 months ago

Hello @sjohnr and @jornfranke I think my problem is the same as you have or had, I didn't get my SAML2 configuration with CSRF in Spring Security 6 working. Based on your experience and context I would be very grateful if you could find the missing or wrong piece. FYI, without saml2, this config is working locally and the XSRF-Token is populated on every request. Could be the filter order faulty?

`@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

    CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
    requestHandler.setCsrfRequestAttributeName(null);

    http.authorizeHttpRequests((authorize) -> authorize
                    .requestMatchers("/actuator/info")
                    .permitAll())
            .authorizeHttpRequests(authorize -> authorize
                    .anyRequest()
                    .authenticated())
            .csrf((csrf) -> csrf
                    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                    .csrfTokenRequestHandler(requestHandler))
            .addFilterAfter(new CsrfCookieFilter(), Saml2WebSsoAuthenticationFilter.class)
            .saml2Login(withDefaults())
            .saml2Logout(withDefaults());

    return http.build();
}

private static final class CsrfCookieFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        csrfToken.getToken();
        filterChain.doFilter(request, response);
    }

}`
jornfranke commented 8 months ago

I am not sure if I can understand your issue correctly. I assume you want to work with a Javascript framework that needs to CRFS token refreshed on every request and has it in its cookies accessible to Javascript for XHR requests. This is an excerpt from working code (it has been according to Spring Boot 3 standards, but maybe not the latest 3.2, but in any case should work.

        CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
        // set the name of the attribute the CsrfToken will be populated on
        delegate.setCsrfRequestAttributeName("_csrf");
        // Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
        // default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
        CsrfTokenRequestHandler requestHandler = delegate::handle;
        http.csrf(
                        (csrf) ->
                                csrf.csrfTokenRepository(tokenRepository)
                                        .csrfTokenRequestHandler(requestHandler))
                .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
        return http.build();
    }
    // We have to add this filter to refresh the CSFR token every time - otherwise the first post
    // after SAML login will fail
    private static final class CsrfCookieFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(
                HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
            // Render the token value to a cookie by causing the deferred token to be loaded
            csrfToken.getToken();
            filterChain.doFilter(request, response);
        }
    }
jornfranke commented 8 months ago

The code has also the improvement that it supports protection against BREACH attacks, which your code did not seem to include (based on XorCsrfTokenRequestAttributeHandler).

jornfranke commented 8 months ago

In the documentation you find this under: https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa

EtienneKaiser commented 8 months ago

Thank you for your answer! You are right, I have a JavaScript (Vue3) framework in place. I do not see any saml2 configured in your code. I guess I do need a BasicAuthenticationFilter if I'm not using BasicAuth. The target is that the csrf Filter is set after the Saml2 filter. Currently there is no Cookie showing up in the Cookie Storage or in the request Headers that is sent with the post request.

Do you have a full working example that have that desired behaviour and is your Cookie showing up in the request headers?

Thanks a lot for your help and guidance!

sjohnr commented 8 months ago

@EtienneKaiser, the code .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class) simply sets the filter order so the custom filter would appear after BasicAuthenticationFilter in the filter chain. This works even if BasicAuthenticationFilter is not present. You don't need to add it for this to work.

Please see the Single-Page Applications section of the CSRF chapter of the docs for more information. If you need additional help, please open a question on stackoverflow. This issue is not intended to be a support forum, and I would ask you to try your best not to use it for that purpose.

jornfranke commented 8 months ago

The example works with SAML2 without changes...

jornfranke commented 8 months ago

like @sjohnr says :)

EtienneKaiser commented 8 months ago

@sjohnr and @jornfranke I wanted to confirm that my config is working with your proposal. To keep it transparent for others who might face the same issue. Thanks for your help, highly appreciated!

nitin3000 commented 4 months ago

Hello Everybody, I am trying to follow the spring angular tutorial present https://spring.io/guides/tutorials/spring-security-and-angular-js/ . I am using spring security 6.1 and Angular 16.1

My security code is

    @Bean
    protected SecurityFilterChain configure (HttpSecurity http) throws Exception {
        CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
        // set the name of the attribute the CsrfToken will be populated on
        delegate.setCsrfRequestAttributeName("_csrf");
        // Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
        // default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
        CsrfTokenRequestHandler requestHandler = delegate::handle;          
        .csrf((csrf) -> csrf
                .csrfTokenRepository(tokenRepository)
                .csrfTokenRequestHandler(requestHandler))
        .authorizeHttpRequests((authz)->authz.requestMatchers("/index.html","/","/home","/login").permitAll()
                .anyRequest().authenticated()
                );
        return http.build();
    }

I have three routes in the Angular application /login (for form based login - this works) /home this makes a rest call and displays some basic data) and logout - (this call invokes a service call to the server, sets authenticate flag in the app to false and redirects to angular route /login) . In the browser developer tools I don't see the "X-XSRF-TOKEN" (the CSRF cookie) I only see the JSESSIONID set in the cookie

In spring configuration, there is no formLogin and httpBasic configured explicitly.

When I go to http://localhost:8080 I am getting a popup ? Should I expect a popup ? if yes then what change do I make to make the popup go away? What are the headers to watch for in the Developer Tools?

jornfranke commented 4 months ago

Difficult to say without the full code. The new Spring default is not to send the CSRF token with every request, but just once. The code above makes sure that it is generated on every request.

Then, you need to also configure Angular to use the CSRF token - in your app.module.ts:

import {
  HttpClientModule,
  HTTP_INTERCEPTORS,
  HttpClientXsrfModule,
} from '@angular/common/http';

...
HttpClientXsrfModule.withOptions({
      cookieName: 'XSRF-TOKEN',
      headerName: 'X-XSRF-TOKEN',
    }),

On the popup: Not sure what you mean with popup, is this related to your Angular code?

I am not sure if I can understand your issue correctly. I assume you want to work with a Javascript framework that needs to CRFS token refreshed on every request and has it in its cookies accessible to Javascript for XHR requests. This is an excerpt from working code (it has been according to Spring Boot 3 standards, but maybe not the latest 3.2, but in any case should work.

        CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        XorCsrfTokenRequestAttributeHandler delegate = new XorCsrfTokenRequestAttributeHandler();
        // set the name of the attribute the CsrfToken will be populated on
        delegate.setCsrfRequestAttributeName("_csrf");
        // Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
        // default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
        CsrfTokenRequestHandler requestHandler = delegate::handle;
        http.csrf(
                        (csrf) ->
                                csrf.csrfTokenRepository(tokenRepository)
                                        .csrfTokenRequestHandler(requestHandler))
                .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
        return http.build();
    }
    // We have to add this filter to refresh the CSFR token every time - otherwise the first post
    // after SAML login will fail
    private static final class CsrfCookieFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(
                HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
            // Render the token value to a cookie by causing the deferred token to be loaded
            csrfToken.getToken();
            filterChain.doFilter(request, response);
        }
    }
nitin3000 commented 4 months ago

Yes, the popup was earlier coming on the / page as the .js files were not under permitAll(). I also see the X-XSRF-TOKEN now. It looks like the issue is when Angular is making some un-authenticated calls to the Back End.