Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
import run.halo.app.core.user.service.RoleService;
import run.halo.app.core.user.service.UserService;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.security.DefaultUserDetailService;
Expand All @@ -51,11 +50,8 @@ public class WebServerSecurityConfig {

@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http,
RoleService roleService,
ObjectProvider<SecurityConfigurer> securityConfigurers,
ServerSecurityContextRepository securityContextRepository,
ReactiveExtensionClient client,
CryptoService cryptoService,
HaloProperties haloProperties,
ServerRequestCache serverRequestCache) {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package run.halo.app.security;

import static run.halo.app.security.HaloServerRequestCache.uriInApplication;

import java.net.URI;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
import org.springframework.security.web.server.ServerRedirectStrategy;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import reactor.core.publisher.Mono;

/**
* This class is responsible for handling the redirection after a successful authentication.
* It checks if a valid 'redirect_uri' query parameter is present in the request. If it is,
* the user is redirected to the specified URI. Otherwise, the user is redirected to a default
* location.
*
* @author johnniang
*/
@Slf4j
public class HaloRedirectAuthenticationSuccessHandler
implements ServerAuthenticationSuccessHandler {

private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();

private final URI location;

public HaloRedirectAuthenticationSuccessHandler(String location) {
this.location = URI.create(location);
}

@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange,
Authentication authentication) {
var request = webFilterExchange.getExchange().getRequest();
var redirectUriQuery = request.getQueryParams()
.getFirst("redirect_uri");
if (redirectUriQuery == null || redirectUriQuery.isBlank()) {
return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), location);
}
var redirectUri = uriInApplication(request, URI.create(redirectUriQuery));
if (log.isDebugEnabled()) {
log.debug(
"Redirecting to: {} after switching to {}",
redirectUri, authentication.getName()
);
}
return redirectStrategy.sendRedirect(
webFilterExchange.getExchange(), URI.create(redirectUri)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ private Mono<Void> saveRedirectUri(ServerWebExchange exchange, URI redirectUri)
.then();
}

private static String uriInApplication(ServerHttpRequest request, URI uri) {
public static String uriInApplication(ServerHttpRequest request, URI uri) {
return uriInApplication(request, uri, true);
}

private static String uriInApplication(
public static String uriInApplication(
ServerHttpRequest request, URI uri, boolean appendFragment
) {
var path = RequestPath.parse(uri, request.getPath().contextPath().value());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authentication.SwitchUserWebFilter;
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
Expand Down Expand Up @@ -55,12 +56,18 @@ SecurityConfigurer unauthenticatedAuthorizationConfigurer() {
@Bean
@Order(200)
SecurityConfigurer preAuthenticationAuthorizationConfigurer() {
return http -> http.authorizeExchange(spec -> spec.pathMatchers(
"/login/**",
"/challenges/**",
"/password-reset/**",
"/signup"
).permitAll());
return http -> http.authorizeExchange(spec -> spec
.pathMatchers("/login/impersonate")
.hasRole(AuthorityUtils.SUPER_ROLE_NAME)
.pathMatchers("/logout/impersonate")
.hasAuthority(SwitchUserWebFilter.ROLE_PREVIOUS_ADMINISTRATOR)
.pathMatchers(
"/login/**",
"/challenges/**",
"/password-reset/**",
"/signup"
)
.permitAll());
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package run.halo.app.security.switchuser;

import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.SwitchUserWebFilter;
import org.springframework.stereotype.Component;
import run.halo.app.security.HaloRedirectAuthenticationSuccessHandler;
import run.halo.app.security.authentication.SecurityConfigurer;

/**
* Switch user configurer.
*
* @author johnniang
*/
@Component
class SwitchUserConfigurer implements SecurityConfigurer {

private final ReactiveUserDetailsService userDetailsService;

SwitchUserConfigurer(ReactiveUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}

@Override
public void configure(ServerHttpSecurity http) {
var successHandler = new HaloRedirectAuthenticationSuccessHandler("/console");
var failureHandler =
new RedirectServerAuthenticationFailureHandler("/login?error=impersonate");
var filter = new SwitchUserWebFilter(userDetailsService, successHandler, failureHandler);
http.addFilterAfter(filter, SecurityWebFiltersOrder.AUTHORIZATION);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package run.halo.app.security.switchuser;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import run.halo.app.security.authorization.AuthorityUtils;


@SpringBootTest
@AutoConfigureWebTestClient
class SwitchUserConfigurerTest {

@Autowired
WebTestClient webClient;

@MockitoSpyBean
ReactiveUserDetailsService userDetailsService;

@Test
void shouldSwitchWithSuperRole() {
when(userDetailsService.findByUsername("faker"))
.thenReturn(Mono.fromSupplier(() -> User.withUsername("faker")
.password("password")
.roles("user")
.build()));
var result = webClient.mutateWith(csrf())
.mutateWith(mockUser("admin").roles(AuthorityUtils.SUPER_ROLE_NAME))
.post()
.uri("/login/impersonate?username={username}&redirect_uri={redirect_uri}",
"faker", "/fake-success"
)
.exchange()
.expectStatus().isFound()
.expectHeader().location("/fake-success")
.expectCookie().exists("SESSION")
.expectBody().returnResult();
var session = result.getResponseCookies().getFirst("SESSION");
assertNotNull(session);

webClient.mutateWith(csrf())
.post().uri("/logout/impersonate?redirect_uri={redirect_uri}", "/fake-logout-success")
.cookie(session.getName(), session.getValue())
.exchange()
.expectStatus().isFound()
.expectHeader().location("/fake-logout-success");
}

@Test
@WithMockUser(username = "admin", roles = "non-super-role")
void shouldNotSwitchWithoutSuperRole() {
webClient.mutateWith(csrf())
.post()
.uri("/login/impersonate?username={username}&redirect_uri={redirect_uri}",
"faker", "/fake-success"
)
.exchange()
.expectStatus().isForbidden();
}

}
Loading