From 4505fcfd168752df1681aebb2701a927a70e088b Mon Sep 17 00:00:00 2001 From: John Niang Date: Mon, 24 Jul 2023 17:26:14 +0800 Subject: [PATCH] Support extending username password authentication (#4265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area core /area plugin #### What this PR does / why we need it: Plugin developers are able to define own UsernamePasswordAuthenticationManager to take charge of username password authentication. 1. If the manager fails to handle, the default authentication manager will be used. 2. If the manager returns `Mono.empty()`, the default authentication manager will be used. For example: ```java @Component public class LdapAuthenticationManager extends UserDetailsRepositoryReactiveAuthenticationManager implements UsernamePasswordAuthenticationManager { public LdapAuthenticationManager(ReactiveUserDetailsService userDetailsService) { super(userDetailsService); } @Override protected Mono retrieveUser(String username) { return super.retrieveUser(username); } } ``` #### Which issue(s) this PR fixes: See https://github.com/halo-dev/halo/issues/4207#issuecomment-1643042348 for more. #### Does this PR introduce a user-facing change? ```release-note 提供用户名密码认证扩展 ``` --- ...UsernamePasswordAuthenticationManager.java | 18 ++++++++ .../login/UsernamePasswordAuthenticator.java | 18 ++++++-- ...sswordDelegatingAuthenticationManager.java | 43 +++++++++++++++++++ .../extensionpoint-definitions.yaml | 11 +++++ 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 api/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordAuthenticationManager.java create mode 100644 application/src/main/java/run/halo/app/security/authentication/login/UsernamePasswordDelegatingAuthenticationManager.java 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."