Skip to content

Commit 1d24f3e

Browse files
committed
Add support for native htmx redirects in Spring Security
See: https://github.com/lcnicolau/cs50-todo-list?tab=readme-ov-file#htmx-redirect-pattern
1 parent d996ac0 commit 1d24f3e

10 files changed

+787
-0
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,38 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
250250
}
251251
```
252252

253+
In addition, htmx provides a special way to send a redirect instruction to the client, keeping a success code ([200](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200)) and sending a custom HTTP header from the server ([HX-Location](https://htmx.org/headers/hx-location/) / [HX-Redirect](https://htmx.org/headers/hx-redirect/)). Htmx correctly interprets these headers and follows the redirect, replacing the response in the page body.
254+
255+
You can take advantage of this behavior by integrating the `HxLocationRedirectAuthenticationFailureHandler`, `HxLocationRedirectAuthenticationSuccessHandler`, `HxLocationRedirectLogoutSuccessHandler`, `HxLocationRedirectAuthenticationEntryPoint` and/or `HxLocationRedirectAccessDeniedHandler` into the `SecurityFilterChain` bean definition.
256+
257+
```java
258+
@Bean
259+
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
260+
// probably some other configurations here
261+
return http
262+
.formLogin(login -> login
263+
.failureHandler(new HxLocationRedirectAuthenticationFailureHandler("/login?failure"))
264+
.successHandler(new HxLocationRedirectAuthenticationSuccessHandler("/home?login"))
265+
).logout(logout -> logout
266+
.logoutSuccessHandler(new HxLocationRedirectLogoutSuccessHandler("/home?logout"))
267+
).exceptionHandling(handler -> handler
268+
.authenticationEntryPoint(new HxLocationRedirectAuthenticationEntryPoint("/login?unauthorized"))
269+
.accessDeniedHandler(new HxLocationRedirectAccessDeniedHandler("/error?forbidden"))
270+
).build();
271+
}
272+
```
273+
274+
Also, you can use the provided `HxLocationBoostedRedirectStrategy` as the second parameter in the handlers, instructing the client to include the [HX-Boosted](https://htmx.org/reference/#headers) header in the new request. This can be useful if you want to take advantage of existing controller optimizations, for example, rendering a fragment instead of the full page for non-boosted, htmx-driven requests:
275+
276+
```java
277+
@GetMapping("/login")
278+
String login(HtmxRequest request) {
279+
return request.isHtmxRequest() && !request.isBoosted()
280+
? "pages/login :: content"
281+
: "pages/login";
282+
}
283+
```
284+
253285
### Thymeleaf
254286

255287
#### Markup Selectors and HTML Fragments
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package io.github.wimdeblauwe.htmx.spring.boot.security;
2+
3+
import io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxLocation;
4+
import io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import org.springframework.http.HttpStatus;
8+
import org.springframework.security.web.DefaultRedirectStrategy;
9+
import tools.jackson.databind.json.JsonMapper;
10+
11+
import java.io.IOException;
12+
import java.util.Map;
13+
14+
import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.HX_BOOSTED;
15+
import static org.springframework.http.HttpStatus.OK;
16+
17+
/**
18+
* htmx-friendly redirect strategy to be used with any security handler that performs redirects.
19+
* <p>
20+
* When instantiated by the default constructor, it checks for htmx requests and responds with {@link HttpStatus#OK},
21+
* including the target URL in the {@link HtmxResponseHeader#HX_LOCATION} header.
22+
* <p>
23+
* It also sets the {@code headers} and {@code target} parameters, instructing the client to include the
24+
* {@code HX-Boosted} header in the new request, and to swap the response into the {@code body} element.
25+
* <p>
26+
* Example:
27+
* <pre> {@code
28+
* HX-Location: {
29+
* "path":"/login?unauthorized",
30+
* "headers":{"HX-Boosted":"true"},
31+
* "target":"body"
32+
* }
33+
* } </pre>
34+
* <p>
35+
* These parameters are useful if you want to take advantage of existing controller optimizations, to render a fragment
36+
* instead of the full page for non-boosted, htmx-driven requests:
37+
* <p>
38+
* <pre> {@code
39+
* @GetMapping("/login")
40+
* String login(HtmxRequest request) {
41+
* return request.isHtmxRequest() && !request.isBoosted()
42+
* ? "pages/login :: content"
43+
* : "pages/login";
44+
* }
45+
* } </pre>
46+
* <p>
47+
* In case you don’t need this "boosted" behavior, use the {@link HxLocationRedirectStrategy} instead.
48+
* <p>
49+
* For non-htmx requests, it delegates to the {@link DefaultRedirectStrategy}.
50+
*
51+
* @author LC Nicolau
52+
* @see <a href="https://htmx.org/headers/hx-location/">HX-Location Response Header</a>
53+
* @see <a href="https://htmx.org/reference/#headers">HTTP Header Reference</a>
54+
* @since 5.0.0
55+
*/
56+
public class HxLocationBoostedRedirectStrategy extends HxLocationRedirectStrategy {
57+
58+
private final JsonMapper jsonMapper;
59+
60+
public HxLocationBoostedRedirectStrategy() {
61+
this(OK);
62+
}
63+
64+
public HxLocationBoostedRedirectStrategy(HttpStatus status) {
65+
super(status);
66+
this.jsonMapper = new JsonMapper();
67+
}
68+
69+
@Override
70+
protected void sendHxLocationRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
71+
super.sendHxLocationRedirect(request, response, boosted(url));
72+
}
73+
74+
protected String boosted(String url) {
75+
HtmxLocation location = new HtmxLocation(url);
76+
location.setTarget("body");
77+
location.setHeaders(Map.of(HX_BOOSTED.getValue(), "true"));
78+
return jsonMapper.writeValueAsString(location);
79+
}
80+
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package io.github.wimdeblauwe.htmx.spring.boot.security;
2+
3+
import jakarta.servlet.ServletException;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.security.access.AccessDeniedException;
8+
import org.springframework.security.web.RedirectStrategy;
9+
import org.springframework.security.web.access.AccessDeniedHandler;
10+
11+
import java.io.IOException;
12+
13+
/**
14+
* Handles navigation on {@link HttpStatus#FORBIDDEN} access by delegating to the {@link HxLocationRedirectStrategy},
15+
* providing an htmx-friendly redirect mechanism.
16+
* <p>
17+
* This class is not used by the library itself, but users of the library can use it to configure their security for
18+
* native htmx redirects.
19+
*
20+
* @author LC Nicolau
21+
* @since 5.0.0
22+
*/
23+
public class HxLocationRedirectAccessDeniedHandler implements AccessDeniedHandler {
24+
25+
private final String redirectUrl;
26+
private final RedirectStrategy redirectStrategy;
27+
28+
public HxLocationRedirectAccessDeniedHandler(String redirectUrl) {
29+
this(redirectUrl, new HxLocationRedirectStrategy(HttpStatus.FORBIDDEN));
30+
}
31+
32+
public HxLocationRedirectAccessDeniedHandler(String redirectUrl, RedirectStrategy redirectStrategy) {
33+
this.redirectUrl = redirectUrl;
34+
this.redirectStrategy = redirectStrategy;
35+
}
36+
37+
@Override
38+
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
39+
redirectStrategy.sendRedirect(request, response, redirectUrl);
40+
}
41+
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package io.github.wimdeblauwe.htmx.spring.boot.security;
2+
3+
import jakarta.servlet.ServletException;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.security.core.AuthenticationException;
8+
import org.springframework.security.web.AuthenticationEntryPoint;
9+
import org.springframework.security.web.RedirectStrategy;
10+
11+
import java.io.IOException;
12+
13+
/**
14+
* Handles navigation on {@link HttpStatus#UNAUTHORIZED} access by delegating to the {@link HxLocationRedirectStrategy},
15+
* providing an htmx-friendly redirect mechanism.
16+
* <p>
17+
* This class is not used by the library itself, but users of the library can use it to configure their security for
18+
* native htmx redirects.
19+
*
20+
* @author LC Nicolau
21+
* @since 5.0.0
22+
*/
23+
public class HxLocationRedirectAuthenticationEntryPoint implements AuthenticationEntryPoint {
24+
25+
private final String redirectUrl;
26+
private final RedirectStrategy redirectStrategy;
27+
28+
public HxLocationRedirectAuthenticationEntryPoint(String redirectUrl) {
29+
this(redirectUrl, new HxLocationRedirectStrategy(HttpStatus.UNAUTHORIZED));
30+
}
31+
32+
public HxLocationRedirectAuthenticationEntryPoint(String redirectUrl, RedirectStrategy redirectStrategy) {
33+
this.redirectUrl = redirectUrl;
34+
this.redirectStrategy = redirectStrategy;
35+
}
36+
37+
@Override
38+
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
39+
redirectStrategy.sendRedirect(request, response, redirectUrl);
40+
}
41+
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.github.wimdeblauwe.htmx.spring.boot.security;
2+
3+
import jakarta.servlet.ServletException;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.security.core.AuthenticationException;
8+
import org.springframework.security.web.RedirectStrategy;
9+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
10+
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
11+
12+
import java.io.IOException;
13+
14+
/**
15+
* Handles a failed authentication attempt by delegating to the {@link SimpleUrlAuthenticationFailureHandler}, using
16+
* {@link HxLocationRedirectStrategy} to provide an htmx-friendly redirect mechanism.
17+
* <p>
18+
* This class is not used by the library itself, but users of the library can use it to configure their security for
19+
* native htmx redirects.
20+
*
21+
* @author LC Nicolau
22+
* @since 5.0.0
23+
*/
24+
public class HxLocationRedirectAuthenticationFailureHandler implements AuthenticationFailureHandler {
25+
26+
private final AuthenticationFailureHandler delegate;
27+
28+
public HxLocationRedirectAuthenticationFailureHandler(String defaultFailureUrl) {
29+
this(defaultFailureUrl, new HxLocationRedirectStrategy(HttpStatus.UNAUTHORIZED));
30+
}
31+
32+
public HxLocationRedirectAuthenticationFailureHandler(String defaultFailureUrl, RedirectStrategy redirectStrategy) {
33+
var handler = new SimpleUrlAuthenticationFailureHandler();
34+
handler.setDefaultFailureUrl(defaultFailureUrl);
35+
handler.setRedirectStrategy(redirectStrategy);
36+
this.delegate = handler;
37+
}
38+
39+
@Override
40+
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
41+
delegate.onAuthenticationFailure(request, response, exception);
42+
}
43+
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.github.wimdeblauwe.htmx.spring.boot.security;
2+
3+
import jakarta.servlet.ServletException;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import org.springframework.security.core.Authentication;
7+
import org.springframework.security.web.RedirectStrategy;
8+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
9+
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
10+
11+
import java.io.IOException;
12+
13+
/**
14+
* Handles post-authentication navigation by delegating to the {@link SavedRequestAwareAuthenticationSuccessHandler},
15+
* using {@link HxLocationRedirectStrategy} to provide an htmx-friendly redirect mechanism.
16+
* <p>
17+
* This class is not used by the library itself, but users of the library can use it to configure their security for
18+
* native htmx redirects.
19+
*
20+
* @author LC Nicolau
21+
* @since 5.0.0
22+
*/
23+
public class HxLocationRedirectAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
24+
25+
private final AuthenticationSuccessHandler delegate;
26+
27+
public HxLocationRedirectAuthenticationSuccessHandler(String defaultSuccessUrl) {
28+
this(defaultSuccessUrl, false);
29+
}
30+
31+
public HxLocationRedirectAuthenticationSuccessHandler(String defaultSuccessUrl, boolean alwaysUse) {
32+
this(defaultSuccessUrl, alwaysUse, new HxLocationRedirectStrategy());
33+
}
34+
35+
public HxLocationRedirectAuthenticationSuccessHandler(String defaultSuccessUrl, RedirectStrategy redirectStrategy) {
36+
this(defaultSuccessUrl, false, redirectStrategy);
37+
}
38+
39+
public HxLocationRedirectAuthenticationSuccessHandler(String defaultSuccessUrl, boolean alwaysUse, RedirectStrategy redirectStrategy) {
40+
var handler = new SavedRequestAwareAuthenticationSuccessHandler();
41+
handler.setDefaultTargetUrl(defaultSuccessUrl);
42+
handler.setAlwaysUseDefaultTargetUrl(alwaysUse);
43+
handler.setRedirectStrategy(redirectStrategy);
44+
this.delegate = handler;
45+
}
46+
47+
@Override
48+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
49+
delegate.onAuthenticationSuccess(request, response, authentication);
50+
}
51+
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.github.wimdeblauwe.htmx.spring.boot.security;
2+
3+
import jakarta.servlet.ServletException;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import org.springframework.security.core.Authentication;
7+
import org.springframework.security.web.RedirectStrategy;
8+
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
9+
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
10+
11+
import java.io.IOException;
12+
13+
/**
14+
* Handles post-logout navigation by delegating to the {@link SimpleUrlLogoutSuccessHandler}, using
15+
* {@link HxLocationRedirectStrategy} to provide an htmx-friendly redirect mechanism.
16+
* <p>
17+
* This class is not used by the library itself, but users of the library can use it to configure their security for
18+
* native htmx redirects.
19+
*
20+
* @author LC Nicolau
21+
* @since 5.0.0
22+
*/
23+
public class HxLocationRedirectLogoutSuccessHandler implements LogoutSuccessHandler {
24+
25+
private final LogoutSuccessHandler delegate;
26+
27+
public HxLocationRedirectLogoutSuccessHandler(String logoutSuccessUrl) {
28+
this(logoutSuccessUrl, false);
29+
}
30+
31+
public HxLocationRedirectLogoutSuccessHandler(String logoutSuccessUrl, boolean alwaysUse) {
32+
this(logoutSuccessUrl, alwaysUse, new HxLocationRedirectStrategy());
33+
}
34+
35+
public HxLocationRedirectLogoutSuccessHandler(String logoutSuccessUrl, RedirectStrategy redirectStrategy) {
36+
this(logoutSuccessUrl, false, redirectStrategy);
37+
}
38+
39+
public HxLocationRedirectLogoutSuccessHandler(String logoutSuccessUrl, boolean alwaysUse, RedirectStrategy redirectStrategy) {
40+
var handler = new SimpleUrlLogoutSuccessHandler();
41+
handler.setDefaultTargetUrl(logoutSuccessUrl);
42+
handler.setAlwaysUseDefaultTargetUrl(alwaysUse);
43+
handler.setRedirectStrategy(redirectStrategy);
44+
this.delegate = handler;
45+
}
46+
47+
@Override
48+
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
49+
delegate.onLogoutSuccess(request, response, authentication);
50+
}
51+
52+
}

0 commit comments

Comments
 (0)