Add support to disable two-factor authentication (#6242)

#### 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
支持通过配置的方式全局禁用二步验证
```
pull/6279/head
John Niang 2024-07-01 17:57:17 +08:00 committed by GitHub
parent 0b7b74e826
commit cc3564bf82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 45 additions and 4 deletions

View File

@ -127,8 +127,12 @@ public class WebServerSecurityConfig {
@Bean @Bean
DefaultUserDetailService userDetailsService(UserService userService, DefaultUserDetailService userDetailsService(UserService userService,
RoleService roleService) { RoleService roleService,
return new DefaultUserDetailService(userService, roleService); HaloProperties haloProperties) {
var userDetailService = new DefaultUserDetailService(userService, roleService);
var twoFactorAuthDisabled = haloProperties.getSecurity().getTwoFactorAuth().isDisabled();
userDetailService.setTwoFactorAuthDisabled(twoFactorAuthDisabled);
return userDetailService;
} }
@Bean @Bean

View File

@ -16,6 +16,18 @@ public class SecurityProperties {
private final RememberMeOptions rememberMe = new RememberMeOptions(); 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 @Data
public static class FrameOptions { public static class FrameOptions {

View File

@ -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.AUTHENTICATED_ROLE_NAME;
import static run.halo.app.security.authorization.AuthorityUtils.ROLE_PREFIX; import static run.halo.app.security.authorization.AuthorityUtils.ROLE_PREFIX;
import lombok.Setter;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
@ -31,6 +32,12 @@ public class DefaultUserDetailService
private final RoleService roleService; private final RoleService roleService;
/**
* Indicates whether two-factor authentication is disabled.
*/
@Setter
private boolean twoFactorAuthDisabled;
public DefaultUserDetailService(UserService userService, RoleService roleService) { public DefaultUserDetailService(UserService userService, RoleService roleService) {
this.userService = userService; this.userService = userService;
this.roleService = roleService; this.roleService = roleService;
@ -66,7 +73,9 @@ public class DefaultUserDetailService
return setAuthorities.then(Mono.fromSupplier(() -> { return setAuthorities.then(Mono.fromSupplier(() -> {
var twoFactorAuthSettings = TwoFactorUtils.getTwoFactorAuthSettings(user); var twoFactorAuthSettings = TwoFactorUtils.getTwoFactorAuthSettings(user);
return new HaloUser.Builder(userBuilder.build()) return new HaloUser.Builder(userBuilder.build())
.twoFactorAuthEnabled(twoFactorAuthSettings.isAvailable()) .twoFactorAuthEnabled(
(!twoFactorAuthDisabled) && twoFactorAuthSettings.isAvailable()
)
.totpEncryptedSecret(user.getSpec().getTotpEncryptedSecret()) .totpEncryptedSecret(user.getSpec().getTotpEncryptedSecret())
.build(); .build();
})); }));

View File

@ -156,11 +156,27 @@ class DefaultUserDetailServiceTest {
.verifyComplete(); .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 @Test
void shouldFindUserDetailsByExistingUsernameButKindOfRoleRefIsNotRole() { void shouldFindUserDetailsByExistingUsernameButKindOfRoleRefIsNotRole() {
var foundUser = createFakeUser(); var foundUser = createFakeUser();
var roleGvk = new Role().groupVersionKind();
var roleRef = new RoleRef(); var roleRef = new RoleRef();
roleRef.setKind("FakeRole"); roleRef.setKind("FakeRole");
roleRef.setApiGroup("fake.halo.run"); roleRef.setApiGroup("fake.halo.run");