Skip to content

Simplify CSRF Configuration for SPAs #14149

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

Closed
jzheaux opened this issue Nov 15, 2023 · 9 comments
Closed

Simplify CSRF Configuration for SPAs #14149

jzheaux opened this issue Nov 15, 2023 · 9 comments
Assignees
Labels
in: web An issue in web modules (web, webmvc) status: duplicate A duplicate of another issue type: enhancement A general enhancement

Comments

@jzheaux
Copy link
Contributor

jzheaux commented Nov 15, 2023

For a SPA, the current recommendation for configuration CSRF is three-fold:

  1. set the CsrfTokenRepository to CsrfTokenRepository#withHttpOnlyFalse
  2. set the CsrfAttributeHandler to a custom class listed in the reference manual
  3. add a custom filter that "subscribes" to the deferred cookie so that the cookie header is written

The current state of the recommendation could be improved in a way that is less error-prone and requires less custom boilerplate for users.

One possibility is to provide a customizer like so:

.csrf(CsrfCustomizers.spaDefaults())

Where said customizer would apply these three rules for them. I imagine this might look something like the following:

public static Customizer<CsrfConfigurer> spaDefaults() {
    return (csrf) -> csrf
        .csrfTokenRepositorySubscription(CookieCsrfTokenRepository.withHttpOnlyFalse())
        .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
    )
}

Where csrfTokenRepositorySubscription is pseudocode for a way to supply the repository and indicate that the filter chain should automatically subscribe to the cookie as part of formulating the response (#3 in the above list) and SpaCsrfTokenRequestHandler is pseudocode for an implementation that is similar to the sample in the reference guide.

@jzheaux
Copy link
Contributor Author

jzheaux commented Nov 15, 2023

Note that this can be achieved with the existing DSL in the following way:

public static final class CsrfCustomizers {
    public static Customizer<CsrfConfigurer> spaDefaults() {
        return (csrf) -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .csrfTokenHandler(new SpaCsrfTokenRequestHandler());
    }

    private static final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
	private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
	private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
		this.xor.handle(request, response, csrfToken);
		csrfToken.get(); // subscribe
	}

	@Override
	public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
		String header = request.getHeader(csrfToken.getHeaderName());
		return ((header != null) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
	}
    }
}

@AugustoRavazoli
Copy link

AugustoRavazoli commented Nov 18, 2023

Will SecurityMockMvcRequestPostProcessors.csrf().asHeader() support this?

@ch4mpy
Copy link
Contributor

ch4mpy commented Nov 27, 2023

Would you kindly expose this SpaCsrfTokenRequestHandler publicly? And maybe do the same for the reactive stack?

Most of my OAuth2 clients (with oauth2Login and consumed by single-page or mobile apps) are reactive because this clients are BFFs implemented with spring-cloud-gateway and the TokenRelay filter. So it would be great to benefit the same configuration simplification on the reactive stack too.

@jzheaux
Copy link
Contributor Author

jzheaux commented Nov 28, 2023

Sure, @ch4mpy. I think that's something we could consider in the context of this ticket. I think one of the main sticky points is whether the implementation should subscribe in such a way as to ensure that the cookie is written without requiring an extra filter. It's a bit easier to hide that choice behind the context of something more opinionated like the DSL.

@ch4mpy
Copy link
Contributor

ch4mpy commented Nov 28, 2023

whether the implementation should subscribe in such a way as to ensure that the cookie is written

@jzheaux, in my opinion, yes: when I use a customizer or configuration option called *Cookie*Repository, I expect that the cookie is computed and added to the request.

With Boot, it's easy to add csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()).csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) in configuration if a property has a given value and to register a filter @Bean with a @ConditionalOnProperty checking for the same property value (this is something I do already).

I have not tried to find a way to do the same with just a customizer, but that would have the advantage to be usable even without Boot (and activating such a customizer based on properties in a Boot starter would be trivial).

@kguelzau
Copy link

Just want to drop a message that this is still needed.

Spent a lot of time in docs and issues figuring out what is the current correct way of to prevent CSRF for SPA in Spring Cloud Gateway (Reactive) with Spring Security.
And I am still unsure...

  • if the issued XSRF-TOKEN Cookie shouldn't be better SameSite = Strict by default
  • if the Cookie should change it's value on each response

@hannah23280
Copy link

hannah23280 commented Apr 7, 2025

I vote for this suggestion!! Please make things simple by reducing boilerplate code, especially for common scenario.

felhag added a commit to felhag/spring-security that referenced this issue Apr 18, 2025
@felhag
Copy link

felhag commented Apr 18, 2025

So I fell into the same bear pit trying to figure out CSRF combined with SPA. Makes a lot of sense to add a sensible default for this imho. I made a PR for this based on the documentation and the comment by @jzheaux. Not sure if this is the right approach though, I'm not an export in spring-security nor in CSRF. But if this is something we'd like to continue I'm happy to update the documentation (edit; and my commit message and formatting apparently) accordingly.

Something I noticed though is I had to add the X-XSRF-TOKEN header and the XSRF-TOKEN cookie for the last test case to succeed. This doesn't seem right does it?

felhag added a commit to felhag/spring-security that referenced this issue Apr 18, 2025
@jzheaux
Copy link
Contributor Author

jzheaux commented May 1, 2025

I'm closing this ticket in favor of working with @felhag through their PR.

@jzheaux jzheaux closed this as completed May 1, 2025
@jzheaux jzheaux added the status: duplicate A duplicate of another issue label May 1, 2025
@jzheaux jzheaux self-assigned this May 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web An issue in web modules (web, webmvc) status: duplicate A duplicate of another issue type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

6 participants