From cc3564bf8292c4420ccd3b432ff5426cfb77e12b Mon Sep 17 00:00:00 2001 From: John Niang Date: Mon, 1 Jul 2024 17:57:17 +0800 Subject: [PATCH] Add support to disable two-factor authentication (#6242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /area core /milestone 2.17.0 #### What this PR does / why we need it: This PR provides a configuration property to control whether two-factor authentication is disabled. e.g.: ```yaml halo: security: two-factor-auth: disabled: true | false # Default is false. ``` #### Which issue(s) this PR fixes: Fixes #5640 #### Special notes for your reviewer: 1. Enable 2FA and configure TOTP 2. Disable 2FA by configuring property above 3. Restart Halo and try to login #### Does this PR introduce a user-facing change? ```release-note 支持通过配置的方式全局禁用二步验证 ``` --- .../app/config/WebServerSecurityConfig.java | 8 ++++++-- .../infra/properties/SecurityProperties.java | 12 ++++++++++++ .../app/security/DefaultUserDetailService.java | 11 ++++++++++- .../security/DefaultUserDetailServiceTest.java | 18 +++++++++++++++++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java b/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java index 17954539a..06c191707 100644 --- a/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java +++ b/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java @@ -127,8 +127,12 @@ public class WebServerSecurityConfig { @Bean DefaultUserDetailService userDetailsService(UserService userService, - RoleService roleService) { - return new DefaultUserDetailService(userService, roleService); + RoleService roleService, + HaloProperties haloProperties) { + var userDetailService = new DefaultUserDetailService(userService, roleService); + var twoFactorAuthDisabled = haloProperties.getSecurity().getTwoFactorAuth().isDisabled(); + userDetailService.setTwoFactorAuthDisabled(twoFactorAuthDisabled); + return userDetailService; } @Bean diff --git a/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java b/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java index 19bfb8bb9..d67b49549 100644 --- a/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java +++ b/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java @@ -16,6 +16,18 @@ public class SecurityProperties { private final RememberMeOptions rememberMe = new RememberMeOptions(); + private final TwoFactorAuthOptions twoFactorAuth = new TwoFactorAuthOptions(); + + @Data + public static class TwoFactorAuthOptions { + + /** + * Whether two-factor authentication is disabled. + */ + private boolean disabled; + + } + @Data public static class FrameOptions { diff --git a/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java b/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java index b56e2625e..9d9a63e9b 100644 --- a/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java +++ b/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java @@ -7,6 +7,7 @@ import static run.halo.app.security.authorization.AuthorityUtils.ANONYMOUS_ROLE_ import static run.halo.app.security.authorization.AuthorityUtils.AUTHENTICATED_ROLE_NAME; import static run.halo.app.security.authorization.AuthorityUtils.ROLE_PREFIX; +import lombok.Setter; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; @@ -31,6 +32,12 @@ public class DefaultUserDetailService private final RoleService roleService; + /** + * Indicates whether two-factor authentication is disabled. + */ + @Setter + private boolean twoFactorAuthDisabled; + public DefaultUserDetailService(UserService userService, RoleService roleService) { this.userService = userService; this.roleService = roleService; @@ -66,7 +73,9 @@ public class DefaultUserDetailService return setAuthorities.then(Mono.fromSupplier(() -> { var twoFactorAuthSettings = TwoFactorUtils.getTwoFactorAuthSettings(user); return new HaloUser.Builder(userBuilder.build()) - .twoFactorAuthEnabled(twoFactorAuthSettings.isAvailable()) + .twoFactorAuthEnabled( + (!twoFactorAuthDisabled) && twoFactorAuthSettings.isAvailable() + ) .totpEncryptedSecret(user.getSpec().getTotpEncryptedSecret()) .build(); })); diff --git a/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java b/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java index a6251fdfb..58aa4fea1 100644 --- a/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java +++ b/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java @@ -156,11 +156,27 @@ class DefaultUserDetailServiceTest { .verifyComplete(); } + @Test + void shouldFindHaloUserDetailsWith2faDisabledWhen2faDisabledGlobally() { + userDetailService.setTwoFactorAuthDisabled(true); + var fakeUser = createFakeUser(); + fakeUser.getSpec().setTwoFactorAuthEnabled(true); + fakeUser.getSpec().setTotpEncryptedSecret("fake-totp-encrypted-secret"); + when(userService.getUser("faker")).thenReturn(Mono.just(fakeUser)); + when(roleService.listRoleRefs(any())).thenReturn(Flux.empty()); + userDetailService.findByUsername("faker") + .as(StepVerifier::create) + .assertNext(userDetails -> { + assertInstanceOf(HaloUserDetails.class, userDetails); + assertFalse(((HaloUserDetails) userDetails).isTwoFactorAuthEnabled()); + }) + .verifyComplete(); + } + @Test void shouldFindUserDetailsByExistingUsernameButKindOfRoleRefIsNotRole() { var foundUser = createFakeUser(); - var roleGvk = new Role().groupVersionKind(); var roleRef = new RoleRef(); roleRef.setKind("FakeRole"); roleRef.setApiGroup("fake.halo.run");