mirror of https://github.com/halo-dev/halo
Add support for serializing or deserializing HaloUser and 2FA (#6005)
#### What type of PR is this? /kind improvement /area core /milestone 2.16.x #### What this PR does / why we need it: This PR adds support for serializing HaloUser and 2FA. 1. Refactor delegate of HaloUser using `org.springframework.security.core.userdetails.User`. 2. Add `HaloSecurityJackson2Module` to enable serialization/deserialization of Halo security module. Below is code snippet of integration: ```java this.objectMapper = Jackson2ObjectMapperBuilder.json() .modules(SecurityJackson2Modules.getModules(this.getClass().getClassLoader())) .modules(modules -> modules.add(new HaloSecurityJackson2Module())) .indentOutput(true) .build(); ``` #### Does this PR introduce a user-facing change? ```release-note None ```pull/6007/head
parent
afabffc546
commit
dad6610cce
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.security;
|
package run.halo.app.security;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNullElse;
|
||||||
import static run.halo.app.core.extension.User.GROUP;
|
import static run.halo.app.core.extension.User.GROUP;
|
||||||
import static run.halo.app.core.extension.User.KIND;
|
import static run.halo.app.core.extension.User.KIND;
|
||||||
import static run.halo.app.security.authorization.AuthorityUtils.ANONYMOUS_ROLE_NAME;
|
import static run.halo.app.security.authorization.AuthorityUtils.ANONYMOUS_ROLE_NAME;
|
||||||
|
@ -48,19 +49,27 @@ public class DefaultUserDetailService
|
||||||
.flatMap(user -> {
|
.flatMap(user -> {
|
||||||
var name = user.getMetadata().getName();
|
var name = user.getMetadata().getName();
|
||||||
var subject = new Subject(KIND, name, GROUP);
|
var subject = new Subject(KIND, name, GROUP);
|
||||||
|
var userBuilder = User.withUsername(name)
|
||||||
var builder = new HaloUser.Builder(user);
|
.password(user.getSpec().getPassword())
|
||||||
|
.disabled(requireNonNullElse(user.getSpec().getDisabled(), false));
|
||||||
var setAuthorities = roleService.listRoleRefs(subject)
|
var setAuthorities = roleService.listRoleRefs(subject)
|
||||||
.filter(this::isRoleRef)
|
.filter(this::isRoleRef)
|
||||||
.map(RoleRef::getName)
|
.map(RoleRef::getName)
|
||||||
// every authenticated user should have authenticated and anonymous roles.
|
// every authenticated user should have authenticated and anonymous roles.
|
||||||
.concatWithValues(AUTHENTICATED_ROLE_NAME, ANONYMOUS_ROLE_NAME)
|
.concatWithValues(AUTHENTICATED_ROLE_NAME, ANONYMOUS_ROLE_NAME)
|
||||||
.map(roleName -> new SimpleGrantedAuthority(ROLE_PREFIX + roleName))
|
.map(roleName -> new SimpleGrantedAuthority(ROLE_PREFIX + roleName))
|
||||||
|
.distinct()
|
||||||
.collectList()
|
.collectList()
|
||||||
.doOnNext(builder::authorities);
|
.doOnNext(userBuilder::authorities);
|
||||||
|
|
||||||
return setAuthorities.then(Mono.fromSupplier(builder::build));
|
return setAuthorities.then(Mono.fromSupplier(() -> {
|
||||||
|
var twoFactorAuthEnabled =
|
||||||
|
requireNonNullElse(user.getSpec().getTwoFactorAuthEnabled(), false);
|
||||||
|
return new HaloUser.Builder(userBuilder.build())
|
||||||
|
.twoFactorAuthEnabled(twoFactorAuthEnabled)
|
||||||
|
.totpEncryptedSecret(user.getSpec().getTotpEncryptedSecret())
|
||||||
|
.build();
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package run.halo.app.security;
|
||||||
|
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
|
public interface HaloUserDetails extends UserDetails {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if two-factor authentication is enabled.
|
||||||
|
*
|
||||||
|
* @return true if two-factor authentication is enabled, false otherwise.
|
||||||
|
*/
|
||||||
|
boolean isTwoFactorAuthEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the encrypted secret of TOTP.
|
||||||
|
*
|
||||||
|
* @return encrypted secret of TOTP.
|
||||||
|
*/
|
||||||
|
String getTotpEncryptedSecret();
|
||||||
|
|
||||||
|
}
|
|
@ -1,113 +1,120 @@
|
||||||
package run.halo.app.security.authentication.login;
|
package run.halo.app.security.authentication.login;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import org.springframework.security.core.CredentialsContainer;
|
import org.springframework.security.core.CredentialsContainer;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import run.halo.app.core.extension.User;
|
import run.halo.app.security.HaloUserDetails;
|
||||||
|
|
||||||
public class HaloUser implements UserDetails, CredentialsContainer {
|
public class HaloUser implements HaloUserDetails, CredentialsContainer {
|
||||||
|
|
||||||
private final User delegate;
|
private final UserDetails delegate;
|
||||||
|
|
||||||
private final Collection<? extends GrantedAuthority> authorities;
|
private final boolean twoFactorAuthEnabled;
|
||||||
|
|
||||||
public HaloUser(User delegate, Collection<? extends GrantedAuthority> authorities) {
|
private String totpEncryptedSecret;
|
||||||
|
|
||||||
|
public HaloUser(UserDetails delegate,
|
||||||
|
boolean twoFactorAuthEnabled,
|
||||||
|
String totpEncryptedSecret) {
|
||||||
Assert.notNull(delegate, "Delegate user must not be null");
|
Assert.notNull(delegate, "Delegate user must not be null");
|
||||||
Assert.notNull(authorities, "Authorities must not be null");
|
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
|
this.twoFactorAuthEnabled = twoFactorAuthEnabled;
|
||||||
this.authorities = authorities.stream()
|
this.totpEncryptedSecret = totpEncryptedSecret;
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.sorted(Comparator.comparing(GrantedAuthority::getAuthority))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public HaloUser(User delegate) {
|
|
||||||
this(delegate, List.of());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
return authorities;
|
return delegate.getAuthorities();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getPassword() {
|
public String getPassword() {
|
||||||
return delegate.getSpec().getPassword();
|
return delegate.getPassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
return delegate.getMetadata().getName();
|
return delegate.getUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isAccountNonExpired() {
|
public boolean isAccountNonExpired() {
|
||||||
return true;
|
return delegate.isAccountNonExpired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isAccountNonLocked() {
|
public boolean isAccountNonLocked() {
|
||||||
return true;
|
return delegate.isAccountNonLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isCredentialsNonExpired() {
|
public boolean isCredentialsNonExpired() {
|
||||||
return true;
|
return delegate.isCredentialsNonExpired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
var disabled = delegate.getSpec().getDisabled();
|
return delegate.isEnabled();
|
||||||
return disabled == null || !disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public User getDelegate() {
|
|
||||||
return delegate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void eraseCredentials() {
|
public void eraseCredentials() {
|
||||||
delegate.getSpec().setPassword(null);
|
if (delegate instanceof CredentialsContainer container) {
|
||||||
|
container.eraseCredentials();
|
||||||
|
}
|
||||||
|
this.totpEncryptedSecret = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
if (obj instanceof HaloUser user) {
|
if (obj instanceof HaloUser user) {
|
||||||
var username = this.delegate.getMetadata().getName();
|
return Objects.equals(this.delegate, user.delegate);
|
||||||
var otherUsername = user.delegate.getMetadata().getName();
|
|
||||||
return username.equals(otherUsername);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return this.delegate.getMetadata().getName().hashCode();
|
return this.delegate.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTwoFactorAuthEnabled() {
|
||||||
|
return this.twoFactorAuthEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTotpEncryptedSecret() {
|
||||||
|
return this.totpEncryptedSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
private final User user;
|
private final UserDetails user;
|
||||||
|
|
||||||
private Collection<? extends GrantedAuthority> authorities;
|
private boolean twoFactorAuthEnabled;
|
||||||
|
|
||||||
public Builder(User user) {
|
private String totpEncryptedSecret;
|
||||||
|
|
||||||
|
public Builder(UserDetails user) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder authorities(Collection<? extends GrantedAuthority> authorities) {
|
public Builder twoFactorAuthEnabled(boolean twoFactorAuthEnabled) {
|
||||||
this.authorities = authorities;
|
this.twoFactorAuthEnabled = twoFactorAuthEnabled;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HaloUser build() {
|
public Builder totpEncryptedSecret(String totpEncryptedSecret) {
|
||||||
return new HaloUser(user, authorities);
|
this.totpEncryptedSecret = totpEncryptedSecret;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HaloUserDetails build() {
|
||||||
|
return new HaloUser(user, twoFactorAuthEnabled, totpEncryptedSecret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||||
|
import run.halo.app.security.HaloUserDetails;
|
||||||
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
||||||
import run.halo.app.security.authentication.twofactor.TwoFactorUtils;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class UsernamePasswordDelegatingAuthenticationManager
|
public class UsernamePasswordDelegatingAuthenticationManager
|
||||||
|
@ -43,13 +43,10 @@ public class UsernamePasswordDelegatingAuthenticationManager
|
||||||
)
|
)
|
||||||
// check if MFA is enabled after authenticated
|
// check if MFA is enabled after authenticated
|
||||||
.map(a -> {
|
.map(a -> {
|
||||||
if (a.getPrincipal() instanceof HaloUser user) {
|
if (a.getPrincipal() instanceof HaloUserDetails user
|
||||||
var twoFactorAuthSettings =
|
&& user.isTwoFactorAuthEnabled()) {
|
||||||
TwoFactorUtils.getTwoFactorAuthSettings(user.getDelegate());
|
|
||||||
if (twoFactorAuthSettings.isAvailable()) {
|
|
||||||
a = new TwoFactorAuthentication(a);
|
a = new TwoFactorAuthentication(a);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return a;
|
return a;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,10 @@ public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandl
|
||||||
.then(webFilterExchange.getChain().filter(webFilterExchange.getExchange()));
|
.then(webFilterExchange.getChain().filter(webFilterExchange.getExchange()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authentication instanceof CredentialsContainer container) {
|
||||||
|
container.eraseCredentials();
|
||||||
|
}
|
||||||
|
|
||||||
ServerWebExchangeMatcher xhrMatcher = exchange -> {
|
ServerWebExchangeMatcher xhrMatcher = exchange -> {
|
||||||
if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With")
|
if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With")
|
||||||
.contains("XMLHttpRequest")) {
|
.contains("XMLHttpRequest")) {
|
||||||
|
@ -87,14 +91,9 @@ public class UsernamePasswordHandler implements ServerAuthenticationSuccessHandl
|
||||||
() -> defaultSuccessHandler.onAuthenticationSuccess(webFilterExchange,
|
() -> defaultSuccessHandler.onAuthenticationSuccess(webFilterExchange,
|
||||||
authentication)
|
authentication)
|
||||||
.then(Mono.empty())))
|
.then(Mono.empty())))
|
||||||
.flatMap(isXhr -> {
|
.flatMap(isXhr -> ServerResponse.ok()
|
||||||
if (authentication instanceof CredentialsContainer container) {
|
|
||||||
container.eraseCredentials();
|
|
||||||
}
|
|
||||||
return ServerResponse.ok()
|
|
||||||
.bodyValue(authentication.getPrincipal())
|
.bodyValue(authentication.getPrincipal())
|
||||||
.flatMap(response -> response.writeTo(exchange, context));
|
.flatMap(response -> response.writeTo(exchange, context))));
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Void> handleAuthenticationException(Throwable exception,
|
private Mono<Void> handleAuthenticationException(Throwable exception,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import run.halo.app.security.authentication.twofactor.totp.TotpAuthenticationFil
|
||||||
public class TwoFactorAuthSecurityConfigurer implements SecurityConfigurer {
|
public class TwoFactorAuthSecurityConfigurer implements SecurityConfigurer {
|
||||||
|
|
||||||
private final ServerSecurityContextRepository securityContextRepository;
|
private final ServerSecurityContextRepository securityContextRepository;
|
||||||
|
|
||||||
private final TotpAuthService totpAuthService;
|
private final TotpAuthService totpAuthService;
|
||||||
|
|
||||||
private final ServerResponse.Context context;
|
private final ServerResponse.Context context;
|
||||||
|
@ -25,8 +26,11 @@ public class TwoFactorAuthSecurityConfigurer implements SecurityConfigurer {
|
||||||
|
|
||||||
public TwoFactorAuthSecurityConfigurer(
|
public TwoFactorAuthSecurityConfigurer(
|
||||||
ServerSecurityContextRepository securityContextRepository,
|
ServerSecurityContextRepository securityContextRepository,
|
||||||
TotpAuthService totpAuthService, ServerResponse.Context context,
|
TotpAuthService totpAuthService,
|
||||||
MessageSource messageSource, RememberMeServices rememberMeServices) {
|
ServerResponse.Context context,
|
||||||
|
MessageSource messageSource,
|
||||||
|
RememberMeServices rememberMeServices
|
||||||
|
) {
|
||||||
this.securityContextRepository = securityContextRepository;
|
this.securityContextRepository = securityContextRepository;
|
||||||
this.totpAuthService = totpAuthService;
|
this.totpAuthService = totpAuthService;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
|
@ -7,6 +7,11 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication token for two-factor authentication.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
public class TwoFactorAuthentication extends AbstractAuthenticationToken {
|
public class TwoFactorAuthentication extends AbstractAuthenticationToken {
|
||||||
|
|
||||||
private final Authentication previous;
|
private final Authentication previous;
|
||||||
|
@ -33,10 +38,12 @@ public class TwoFactorAuthentication extends AbstractAuthenticationToken {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isAuthenticated() {
|
public boolean isAuthenticated() {
|
||||||
|
// return true for accessing anonymous resources
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Authentication getPrevious() {
|
public Authentication getPrevious() {
|
||||||
return previous;
|
return previous;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
import org.springframework.security.authentication.ReactiveAuthenticationManager;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.CredentialsContainer;
|
||||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
||||||
|
@ -17,7 +18,7 @@ import org.springframework.security.web.server.context.ServerSecurityContextRepo
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.security.authentication.login.HaloUser;
|
import run.halo.app.security.HaloUserDetails;
|
||||||
import run.halo.app.security.authentication.login.UsernamePasswordHandler;
|
import run.halo.app.security.authentication.login.UsernamePasswordHandler;
|
||||||
import run.halo.app.security.authentication.rememberme.RememberMeServices;
|
import run.halo.app.security.authentication.rememberme.RememberMeServices;
|
||||||
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
||||||
|
@ -25,11 +26,13 @@ import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TotpAuthenticationFilter extends AuthenticationWebFilter {
|
public class TotpAuthenticationFilter extends AuthenticationWebFilter {
|
||||||
|
|
||||||
public TotpAuthenticationFilter(ServerSecurityContextRepository securityContextRepository,
|
public TotpAuthenticationFilter(
|
||||||
|
ServerSecurityContextRepository securityContextRepository,
|
||||||
TotpAuthService totpAuthService,
|
TotpAuthService totpAuthService,
|
||||||
ServerResponse.Context context,
|
ServerResponse.Context context,
|
||||||
MessageSource messageSource,
|
MessageSource messageSource,
|
||||||
RememberMeServices rememberMeServices) {
|
RememberMeServices rememberMeServices
|
||||||
|
) {
|
||||||
super(new TwoFactorAuthManager(totpAuthService));
|
super(new TwoFactorAuthManager(totpAuthService));
|
||||||
|
|
||||||
setSecurityContextRepository(securityContextRepository);
|
setSecurityContextRepository(securityContextRepository);
|
||||||
|
@ -96,36 +99,38 @@ public class TotpAuthenticationFilter extends AuthenticationWebFilter {
|
||||||
// it should be TotpAuthenticationToken
|
// it should be TotpAuthenticationToken
|
||||||
var code = (Integer) authentication.getCredentials();
|
var code = (Integer) authentication.getCredentials();
|
||||||
log.debug("Got TOTP code {}", code);
|
log.debug("Got TOTP code {}", code);
|
||||||
|
|
||||||
// get user details
|
// get user details
|
||||||
return ReactiveSecurityContextHolder.getContext()
|
return ReactiveSecurityContextHolder.getContext()
|
||||||
.map(SecurityContext::getAuthentication)
|
.map(SecurityContext::getAuthentication)
|
||||||
.cast(TwoFactorAuthentication.class)
|
.cast(TwoFactorAuthentication.class)
|
||||||
.map(TwoFactorAuthentication::getPrevious)
|
.map(TwoFactorAuthentication::getPrevious)
|
||||||
.<Authentication>handle((prevAuth, sink) -> {
|
.flatMap(previousAuth -> {
|
||||||
var principal = prevAuth.getPrincipal();
|
var principal = previousAuth.getPrincipal();
|
||||||
if (!(principal instanceof HaloUser haloUser)) {
|
if (!(principal instanceof HaloUserDetails user)) {
|
||||||
sink.error(new TwoFactorAuthException("Invalid MFA authentication."));
|
return Mono.error(
|
||||||
return;
|
new TwoFactorAuthException("Invalid authentication principal.")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
var encryptedSecret =
|
var totpEncryptedSecret = user.getTotpEncryptedSecret();
|
||||||
haloUser.getDelegate().getSpec().getTotpEncryptedSecret();
|
if (StringUtils.isBlank(totpEncryptedSecret)) {
|
||||||
if (StringUtils.isBlank(encryptedSecret)) {
|
return Mono.error(
|
||||||
sink.error(new TwoFactorAuthException("Empty secret configured"));
|
new TwoFactorAuthException("TOTP secret not configured.")
|
||||||
return;
|
);
|
||||||
}
|
}
|
||||||
var rawSecret = totpAuthService.decryptSecret(encryptedSecret);
|
var rawSecret = totpAuthService.decryptSecret(totpEncryptedSecret);
|
||||||
var validated = totpAuthService.validateTotp(rawSecret, code);
|
var validated = totpAuthService.validateTotp(rawSecret, code);
|
||||||
if (!validated) {
|
if (!validated) {
|
||||||
sink.error(new TwoFactorAuthException("Invalid TOTP code " + code));
|
return Mono.error(new TwoFactorAuthException("Invalid TOTP code " + code));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
sink.next(prevAuth);
|
|
||||||
})
|
|
||||||
.doOnNext(previousAuth -> {
|
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("TOTP authentication for {} with code {} successfully.",
|
log.debug("TOTP authentication for {} with code {} successfully.",
|
||||||
previousAuth.getName(), code);
|
previousAuth.getName(), code);
|
||||||
}
|
}
|
||||||
|
if (previousAuth instanceof CredentialsContainer container) {
|
||||||
|
container.eraseCredentials();
|
||||||
|
}
|
||||||
|
return Mono.just(previousAuth);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package run.halo.app.security.jackson2;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.Version;
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
|
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||||
|
import run.halo.app.security.authentication.login.HaloUser;
|
||||||
|
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Halo security Jackson2 module.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public class HaloSecurityJackson2Module extends SimpleModule {
|
||||||
|
|
||||||
|
public HaloSecurityJackson2Module() {
|
||||||
|
super(HaloSecurityJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setupModule(SetupContext context) {
|
||||||
|
SecurityJackson2Modules.enableDefaultTyping(context.getOwner());
|
||||||
|
context.setMixInAnnotations(HaloUser.class, HaloUserMixin.class);
|
||||||
|
context.setMixInAnnotations(TwoFactorAuthentication.class,
|
||||||
|
TwoFactorAuthenticationMixin.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package run.halo.app.security.jackson2;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
|
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||||
|
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility =
|
||||||
|
JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
abstract class HaloUserMixin {
|
||||||
|
|
||||||
|
HaloUserMixin(@JsonProperty("delegate") UserDetails delegate,
|
||||||
|
@JsonProperty("twoFactorAuthEnabled") boolean twoFactorAuthEnabled,
|
||||||
|
@JsonProperty("totpEncryptedSecret") String totpEncryptedSecret) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package run.halo.app.security.jackson2;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This mixin class is used to serialize/deserialize TwoFactorAuthentication.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||||
|
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,
|
||||||
|
getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||||
|
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
abstract class TwoFactorAuthenticationMixin {
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
TwoFactorAuthenticationMixin(@JsonProperty("previous") Authentication previous) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package run.halo.app.security.jackson2;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.context.SecurityContextImpl;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.jackson2.SecurityJackson2Modules;
|
||||||
|
import run.halo.app.security.authentication.login.HaloUser;
|
||||||
|
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
||||||
|
|
||||||
|
class HaloSecurityJacksonModuleTest {
|
||||||
|
|
||||||
|
ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
this.objectMapper = Jackson2ObjectMapperBuilder.json()
|
||||||
|
.modules(SecurityJackson2Modules.getModules(this.getClass().getClassLoader()))
|
||||||
|
.modules(modules -> modules.add(new HaloSecurityJackson2Module()))
|
||||||
|
.indentOutput(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void codecHaloUserTest() throws JsonProcessingException {
|
||||||
|
codecAssert(haloUser -> UsernamePasswordAuthenticationToken.authenticated(haloUser,
|
||||||
|
haloUser.getPassword(),
|
||||||
|
haloUser.getAuthorities()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void codecTwoFactorAuthenticationTokenTest() throws JsonProcessingException {
|
||||||
|
codecAssert(haloUser -> new TwoFactorAuthentication(
|
||||||
|
UsernamePasswordAuthenticationToken.authenticated(haloUser,
|
||||||
|
haloUser.getPassword(),
|
||||||
|
haloUser.getAuthorities())));
|
||||||
|
}
|
||||||
|
|
||||||
|
void codecAssert(Function<HaloUser, Authentication> authenticationConverter)
|
||||||
|
throws JsonProcessingException {
|
||||||
|
var userDetails = User.withUsername("faker")
|
||||||
|
.password("123456")
|
||||||
|
.authorities("ROLE_USER")
|
||||||
|
.build();
|
||||||
|
var haloUser = new HaloUser(userDetails, true, "fake-encrypted-secret");
|
||||||
|
|
||||||
|
var authentication = authenticationConverter.apply(haloUser);
|
||||||
|
|
||||||
|
var securityContext = new SecurityContextImpl(authentication);
|
||||||
|
var securityContextJson = objectMapper.writeValueAsString(securityContext);
|
||||||
|
|
||||||
|
var deserializedSecurityContext =
|
||||||
|
objectMapper.readValue(securityContextJson, SecurityContext.class);
|
||||||
|
|
||||||
|
assertEquals(deserializedSecurityContext, securityContext);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue