diff --git a/api/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticationManager.java b/api/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticationManager.java new file mode 100644 index 000000000..e76a9af7a --- /dev/null +++ b/api/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticationManager.java @@ -0,0 +1,18 @@ +package run.halo.app.security.authentication.login; + +import org.pf4j.ExtensionPoint; +import org.springframework.security.authentication.ReactiveAuthenticationManager; + +/** + * An extension point for username password authentication. + * Any non-authentication exception occurs, the default authentication will be used. + * If you want to skip authentication, please return Mono.empty() directly, the default + * authentication will be used. + * + * @author johnniang + * @since 2.8 + */ +public interface UsernamePasswordAuthenticationManager + extends ReactiveAuthenticationManager, ExtensionPoint { + +} diff --git a/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticator.java b/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticator.java index fda714ffb..26479199e 100644 --- a/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticator.java +++ b/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticator.java @@ -40,6 +40,7 @@ import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import run.halo.app.infra.exception.RateLimitExceededException; import run.halo.app.infra.utils.IpAddressUtils; +import run.halo.app.plugin.extensionpoint.ExtensionGetter; import run.halo.app.security.AdditionalWebFilter; /** @@ -71,11 +72,14 @@ public class UsernamePasswordAuthenticator implements AdditionalWebFilter { private final RateLimiterRegistry rateLimiterRegistry; private final MessageSource messageSource; + private final ExtensionGetter extensionGetter; + public UsernamePasswordAuthenticator(ServerResponse.Context context, ObservationRegistry observationRegistry, ReactiveUserDetailsService userDetailsService, ReactiveUserDetailsPasswordService passwordService, PasswordEncoder passwordEncoder, ServerSecurityContextRepository securityContextRepository, CryptoService cryptoService, - RateLimiterRegistry rateLimiterRegistry, MessageSource messageSource) { + RateLimiterRegistry rateLimiterRegistry, MessageSource messageSource, + ExtensionGetter extensionGetter) { this.context = context; this.observationRegistry = observationRegistry; this.userDetailsService = userDetailsService; @@ -85,6 +89,7 @@ public class UsernamePasswordAuthenticator implements AdditionalWebFilter { this.cryptoService = cryptoService; this.rateLimiterRegistry = rateLimiterRegistry; this.messageSource = messageSource; + this.extensionGetter = extensionGetter; this.authenticationWebFilter = new UsernamePasswordAuthenticationWebFilter(authenticationManager()); @@ -113,12 +118,17 @@ public class UsernamePasswordAuthenticator implements AdditionalWebFilter { } ReactiveAuthenticationManager authenticationManager() { - var manager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService); - manager.setPasswordEncoder(passwordEncoder); - manager.setUserDetailsPasswordService(passwordService); + var manager = new UsernamePasswordDelegatingAuthenticationManager(extensionGetter, + defaultAuthenticationManager()); return new ObservationReactiveAuthenticationManager(observationRegistry, manager); } + ReactiveAuthenticationManager defaultAuthenticationManager() { + var manager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService); + manager.setPasswordEncoder(passwordEncoder); + manager.setUserDetailsPasswordService(passwordService); + return manager; + } private RateLimiterOperator createIPBasedRateLimiter(ServerWebExchange exchange) { var clientIp = IpAddressUtils.getClientIp(exchange.getRequest()); diff --git a/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordDelegatingAuthenticationManager.java b/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordDelegatingAuthenticationManager.java new file mode 100644 index 000000000..fe1b25316 --- /dev/null +++ b/application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordDelegatingAuthenticationManager.java @@ -0,0 +1,43 @@ +package run.halo.app.security.authentication.login; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import reactor.core.publisher.Mono; +import run.halo.app.plugin.extensionpoint.ExtensionGetter; + +@Slf4j +public class UsernamePasswordDelegatingAuthenticationManager + implements ReactiveAuthenticationManager { + + private final ExtensionGetter extensionGetter; + + private final ReactiveAuthenticationManager defaultAuthenticationManager; + + public UsernamePasswordDelegatingAuthenticationManager(ExtensionGetter extensionGetter, + ReactiveAuthenticationManager defaultAuthenticationManager) { + this.extensionGetter = extensionGetter; + this.defaultAuthenticationManager = defaultAuthenticationManager; + } + + @Override + public Mono authenticate(Authentication authentication) { + return extensionGetter + .getEnabledExtensionByDefinition(UsernamePasswordAuthenticationManager.class) + .next() + .flatMap(authenticationManager -> authenticationManager.authenticate(authentication) + .doOnError(t -> log.error( + "failed to authenticate with {}, fallback to default username password " + + "authentication.", authenticationManager.getClass(), t) + ) + .onErrorResume( + t -> !(t instanceof AuthenticationException), + t -> Mono.empty() + ) + ) + .switchIfEmpty( + Mono.defer(() -> defaultAuthenticationManager.authenticate(authentication)) + ); + } +} diff --git a/application/src/main/resources/extensions/extensionpoint-definitions.yaml b/application/src/main/resources/extensions/extensionpoint-definitions.yaml index 375b24504..be473f68e 100644 --- a/application/src/main/resources/extensions/extensionpoint-definitions.yaml +++ b/application/src/main/resources/extensions/extensionpoint-definitions.yaml @@ -41,3 +41,14 @@ spec: displayName: CommentWidget type: SINGLETON description: "Provides an extension point for the comment widget on the theme-side." + +--- +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: username-password-authentication-manager +spec: + className: run.halo.app.security.authentication.login.UsernamePasswordAuthenticationManager + displayName: Username password authentication manager + type: SINGLETON + description: "Provides a way to extend the username password authentication."