mirror of https://github.com/halo-dev/halo
Support binding OAuth2 user automatically (#6702)
#### What type of PR is this? /kind improvement /area core /milestone 2.20.x #### What this PR does / why we need it: This PR add support for binding OAuth2 user automatically. So we can remove the user-binding page. Please note that those changes may break the OAuth2 and SocialLogin plugins. #### Special notes for your reviewer: Build OAuth2 plugin from <https://github.com/halo-sigs/plugin-oauth2/pull/64> or use [plugin-oauth2-1.0.4-SNAPSHOT.zip](https://github.com/user-attachments/files/17177592/plugin-oauth2-1.0.4-SNAPSHOT.zip) I built. - Bind after logging in 1. Log in Halo with username and password method 2. Try to unbind OAuth2 user 3. Bind OAuth2 user again - Initially bind without logging in 1. Go to login page 2. Log in with OAuth2 method and you will be redirected to login page 3. Log in with username and password method 4. See the result of binding - Log in with OAuth2 method after binding 1. Go to login page 2. Log in with OAuth2 method and you will be redirected to uc page directly #### Does this PR introduce a user-facing change? ```release-note 支持自动绑定 OAuth2 登录用户 ```pull/6734/head
parent
83109d0568
commit
e77954313d
|
@ -48,36 +48,9 @@ public class UserConnection extends AbstractExtension {
|
|||
private String providerUserId;
|
||||
|
||||
/**
|
||||
* The display name for the user's connection to the OAuth provider.
|
||||
* The time when the user connection was last updated.
|
||||
*/
|
||||
@Schema(requiredMode = REQUIRED)
|
||||
private String displayName;
|
||||
|
||||
/**
|
||||
* The URL to the user's profile page on the OAuth provider.
|
||||
* For example, the user's GitHub profile URL.
|
||||
*/
|
||||
private String profileUrl;
|
||||
|
||||
/**
|
||||
* The URL to the user's avatar image on the OAuth provider.
|
||||
* For example, the user's GitHub avatar URL.
|
||||
*/
|
||||
private String avatarUrl;
|
||||
|
||||
/**
|
||||
* The access token provided by the OAuth provider.
|
||||
*/
|
||||
@Schema(requiredMode = REQUIRED)
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* The refresh token provided by the OAuth provider (if applicable).
|
||||
*/
|
||||
private String refreshToken;
|
||||
|
||||
private Instant expiresAt;
|
||||
|
||||
private Instant updatedAt;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package run.halo.app.security;
|
||||
|
||||
import org.pf4j.ExtensionPoint;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
|
||||
/**
|
||||
* Security web filter for HTTP basic.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
public interface HttpBasicSecurityWebFilter extends WebFilter, ExtensionPoint {
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package run.halo.app.security;
|
||||
|
||||
import org.pf4j.ExtensionPoint;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
|
||||
/**
|
||||
* Security web filter for OAuth2 authorization code.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
public interface OAuth2AuthorizationCodeSecurityWebFilter extends WebFilter, ExtensionPoint {
|
||||
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package run.halo.app.security.authentication.oauth2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import lombok.Getter;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
|
||||
/**
|
||||
* Halo OAuth2 authentication token which combines {@link UserDetails} and original
|
||||
* {@link OAuth2AuthenticationToken}.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
public class HaloOAuth2AuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
@Getter
|
||||
private final UserDetails userDetails;
|
||||
|
||||
@Getter
|
||||
private final OAuth2AuthenticationToken original;
|
||||
|
||||
/**
|
||||
* Constructs an {@code HaloOAuth2AuthenticationToken} using {@link UserDetails} and original
|
||||
* {@link OAuth2AuthenticationToken}.
|
||||
*
|
||||
* @param userDetails the {@link UserDetails}
|
||||
* @param original the original {@link OAuth2AuthenticationToken}
|
||||
*/
|
||||
public HaloOAuth2AuthenticationToken(UserDetails userDetails,
|
||||
OAuth2AuthenticationToken original) {
|
||||
super(combineAuthorities(userDetails, original));
|
||||
this.userDetails = userDetails;
|
||||
this.original = original;
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return userDetails.getUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<GrantedAuthority> getAuthorities() {
|
||||
var originalAuthorities = super.getAuthorities();
|
||||
var userDetailsAuthorities = getUserDetails().getAuthorities();
|
||||
var authorities = new ArrayList<GrantedAuthority>(
|
||||
originalAuthorities.size() + userDetailsAuthorities.size()
|
||||
);
|
||||
authorities.addAll(originalAuthorities);
|
||||
authorities.addAll(userDetailsAuthorities);
|
||||
return Collections.unmodifiableList(authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2User getPrincipal() {
|
||||
return original.getPrincipal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an authenticated {@link HaloOAuth2AuthenticationToken} using {@link UserDetails} and
|
||||
* original {@link OAuth2AuthenticationToken}.
|
||||
*
|
||||
* @param userDetails the {@link UserDetails}
|
||||
* @param original the original {@link OAuth2AuthenticationToken}
|
||||
* @return an authenticated {@link HaloOAuth2AuthenticationToken}
|
||||
*/
|
||||
public static HaloOAuth2AuthenticationToken authenticated(
|
||||
UserDetails userDetails, OAuth2AuthenticationToken original
|
||||
) {
|
||||
return new HaloOAuth2AuthenticationToken(userDetails, original);
|
||||
}
|
||||
|
||||
private static Collection<? extends GrantedAuthority> combineAuthorities(
|
||||
UserDetails userDetails, OAuth2AuthenticationToken original) {
|
||||
var userDetailsAuthorities = userDetails.getAuthorities();
|
||||
var originalAuthorities = original.getAuthorities();
|
||||
var authorities = new ArrayList<GrantedAuthority>(
|
||||
originalAuthorities.size() + userDetailsAuthorities.size()
|
||||
);
|
||||
authorities.addAll(originalAuthorities);
|
||||
authorities.addAll(userDetailsAuthorities);
|
||||
return Collections.unmodifiableList(authorities);
|
||||
}
|
||||
|
||||
}
|
|
@ -88,7 +88,6 @@ import run.halo.app.infra.ValidationUtils;
|
|||
import run.halo.app.infra.exception.RateLimitExceededException;
|
||||
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
|
@ -600,7 +599,7 @@ public class UserEndpoint implements CustomEndpoint {
|
|||
Mono<ServerResponse> me(ServerRequest request) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.filter(auth -> !(auth instanceof TwoFactorAuthentication))
|
||||
.filter(Authentication::isAuthenticated)
|
||||
.flatMap(auth -> userService.getUser(auth.getName())
|
||||
.flatMap(user -> {
|
||||
var roleNames = authoritiesToRoles(auth.getAuthorities());
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package run.halo.app.core.user.service;
|
||||
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.UserConnection;
|
||||
|
||||
public interface UserConnectionService {
|
||||
|
||||
/**
|
||||
* Create user connection.
|
||||
*
|
||||
* @param username Username
|
||||
* @param registrationId Registration id
|
||||
* @param oauth2User OAuth2 user
|
||||
* @return Created user connection
|
||||
*/
|
||||
Mono<UserConnection> createUserConnection(
|
||||
String username,
|
||||
String registrationId,
|
||||
OAuth2User oauth2User
|
||||
);
|
||||
|
||||
/**
|
||||
* Update the user connection if present.
|
||||
* If found, update updatedAt timestamp of the user connection.
|
||||
*
|
||||
* @param registrationId Registration id
|
||||
* @param oauth2User OAuth2 user
|
||||
* @return Updated user connection or empty
|
||||
*/
|
||||
Mono<UserConnection> updateUserConnectionIfPresent(
|
||||
String registrationId, OAuth2User oauth2User
|
||||
);
|
||||
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package run.halo.app.core.user.service.impl;
|
||||
|
||||
import static run.halo.app.extension.ExtensionUtil.defaultSort;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.and;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.UserConnection;
|
||||
import run.halo.app.core.extension.UserConnection.UserConnectionSpec;
|
||||
import run.halo.app.core.user.service.UserConnectionService;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.exception.OAuth2UserAlreadyBoundException;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
@Service
|
||||
public class UserConnectionServiceImpl implements UserConnectionService {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private Clock clock = Clock.systemDefaultZone();
|
||||
|
||||
public UserConnectionServiceImpl(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
void setClock(Clock clock) {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<UserConnection> createUserConnection(
|
||||
String username,
|
||||
String registrationId,
|
||||
OAuth2User oauth2User
|
||||
) {
|
||||
return getUserConnection(registrationId, username)
|
||||
.flatMap(connection -> Mono.<UserConnection>error(
|
||||
() -> new OAuth2UserAlreadyBoundException(connection))
|
||||
)
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
var connection = new UserConnection();
|
||||
connection.setMetadata(new Metadata());
|
||||
var metadata = connection.getMetadata();
|
||||
updateUserInfo(metadata, oauth2User);
|
||||
metadata.setGenerateName(username + "-");
|
||||
connection.setSpec(new UserConnectionSpec());
|
||||
var spec = connection.getSpec();
|
||||
spec.setUsername(username);
|
||||
spec.setProviderUserId(oauth2User.getName());
|
||||
spec.setRegistrationId(registrationId);
|
||||
spec.setUpdatedAt(clock.instant());
|
||||
return client.create(connection);
|
||||
}));
|
||||
}
|
||||
|
||||
private Mono<UserConnection> updateUserConnection(UserConnection connection,
|
||||
OAuth2User oauth2User) {
|
||||
connection.getSpec().setUpdatedAt(clock.instant());
|
||||
updateUserInfo(connection.getMetadata(), oauth2User);
|
||||
return client.update(connection);
|
||||
}
|
||||
|
||||
private Mono<UserConnection> getUserConnection(String registrationId, String username) {
|
||||
var listOptions = ListOptions.builder()
|
||||
.fieldQuery(and(
|
||||
equal("spec.registrationId", registrationId),
|
||||
equal("spec.username", username)
|
||||
))
|
||||
.build();
|
||||
return client.listAll(UserConnection.class, listOptions, defaultSort()).next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<UserConnection> updateUserConnectionIfPresent(String registrationId,
|
||||
OAuth2User oauth2User) {
|
||||
var listOptions = ListOptions.builder()
|
||||
.fieldQuery(and(
|
||||
equal("spec.registrationId", registrationId),
|
||||
equal("spec.providerUserId", oauth2User.getName())
|
||||
))
|
||||
.build();
|
||||
return client.listAll(UserConnection.class, listOptions, defaultSort()).next()
|
||||
.flatMap(connection -> updateUserConnection(connection, oauth2User));
|
||||
}
|
||||
|
||||
private void updateUserInfo(MetadataOperator metadata, OAuth2User oauth2User) {
|
||||
var annotations = Optional.ofNullable(metadata.getAnnotations())
|
||||
.orElseGet(HashMap::new);
|
||||
metadata.setAnnotations(annotations);
|
||||
annotations.put(
|
||||
"auth.halo.run/oauth2-user-info",
|
||||
JsonUtils.objectToJson(oauth2User.getAttributes())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -630,6 +630,22 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
|
|||
.map(UserConnectionSpec::getUsername)
|
||||
.orElse(null)
|
||||
)));
|
||||
is.add(new IndexSpec()
|
||||
.setName("spec.registrationId")
|
||||
.setIndexFunc(simpleAttribute(UserConnection.class,
|
||||
connection -> Optional.ofNullable(connection.getSpec())
|
||||
.map(UserConnectionSpec::getRegistrationId)
|
||||
.orElse(null)
|
||||
))
|
||||
);
|
||||
is.add(new IndexSpec()
|
||||
.setName("spec.providerUserId")
|
||||
.setIndexFunc(simpleAttribute(UserConnection.class,
|
||||
connection -> Optional.ofNullable(connection.getSpec())
|
||||
.map(UserConnectionSpec::getProviderUserId)
|
||||
.orElse(null)
|
||||
))
|
||||
);
|
||||
});
|
||||
|
||||
// security.halo.run
|
||||
|
|
|
@ -2,9 +2,7 @@ package run.halo.app.infra.config;
|
|||
|
||||
import static org.springframework.util.ResourceUtils.FILE_URL_PREFIX;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.method;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
|
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||
import static run.halo.app.infra.utils.FileUtils.checkDirectoryTraversal;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
@ -18,7 +16,6 @@ import org.springframework.context.annotation.Bean;
|
|||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
|
@ -30,15 +27,14 @@ import org.springframework.web.filter.reactive.ServerWebExchangeContextFilter;
|
|||
import org.springframework.web.reactive.config.ResourceHandlerRegistration;
|
||||
import org.springframework.web.reactive.config.ResourceHandlerRegistry;
|
||||
import org.springframework.web.reactive.config.WebFluxConfigurer;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.reactive.resource.EncodedResourceResolver;
|
||||
import org.springframework.web.reactive.resource.PathResourceResolver;
|
||||
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
|
||||
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.endpoint.WebSocketHandlerMapping;
|
||||
import run.halo.app.core.endpoint.console.CustomEndpointsBuilder;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
|
@ -126,34 +122,33 @@ public class WebFluxConfig implements WebFluxConfigurer {
|
|||
}
|
||||
|
||||
@Bean
|
||||
RouterFunction<ServerResponse> consoleIndexRedirection() {
|
||||
var consolePredicate = method(HttpMethod.GET)
|
||||
.and(path("/console/**").and(path("/console/assets/**").negate()))
|
||||
RouterFunction<ServerResponse> consoleEndpoints() {
|
||||
var consolePredicate = path("/console/**").and(path("/console/assets/**").negate())
|
||||
.and(accept(MediaType.TEXT_HTML))
|
||||
.and(new WebSocketRequestPredicate().negate());
|
||||
return route(consolePredicate,
|
||||
request -> this.serveIndex(haloProp.getConsole().getLocation() + "index.html"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
RouterFunction<ServerResponse> ucIndexRedirect() {
|
||||
var consolePredicate = method(HttpMethod.GET)
|
||||
.and(path("/uc/**").and(path("/uc/assets/**").negate()))
|
||||
var ucPredicate = path("/uc/**").and(path("/uc/assets/**").negate())
|
||||
.and(accept(MediaType.TEXT_HTML))
|
||||
.and(new WebSocketRequestPredicate().negate());
|
||||
return route(consolePredicate,
|
||||
request -> this.serveIndex(haloProp.getUc().getLocation() + "index.html"));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> serveIndex(String indexLocation) {
|
||||
var indexResource = applicationContext.getResource(indexLocation);
|
||||
try {
|
||||
return ServerResponse.ok()
|
||||
.cacheControl(CacheControl.noStore())
|
||||
.body(BodyInserters.fromResource(indexResource));
|
||||
} catch (Throwable e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
var consoleIndexHtml =
|
||||
applicationContext.getResource(haloProp.getConsole().getLocation() + "index.html");
|
||||
|
||||
var ucIndexHtml =
|
||||
applicationContext.getResource(haloProp.getUc().getLocation() + "index.html");
|
||||
|
||||
return RouterFunctions.route()
|
||||
.GET(consolePredicate,
|
||||
request -> ServerResponse.ok()
|
||||
.cacheControl(CacheControl.noStore())
|
||||
.bodyValue(consoleIndexHtml)
|
||||
)
|
||||
.GET(ucPredicate,
|
||||
request -> ServerResponse.ok()
|
||||
.cacheControl(CacheControl.noStore())
|
||||
.bodyValue(ucIndexHtml)
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -4,6 +4,7 @@ import static org.springframework.security.config.Customizer.withDefaults;
|
|||
import static org.springframework.security.web.server.authentication.ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.builder;
|
||||
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
|
@ -12,17 +13,24 @@ import org.springframework.boot.autoconfigure.web.ServerProperties;
|
|||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
|
||||
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||
import org.springframework.session.MapSession;
|
||||
import org.springframework.session.config.annotation.web.server.EnableSpringWebSession;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.user.service.RoleService;
|
||||
import run.halo.app.core.user.service.UserService;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
@ -34,7 +42,7 @@ import run.halo.app.security.authentication.SecurityConfigurer;
|
|||
import run.halo.app.security.authentication.impl.RsaKeyService;
|
||||
import run.halo.app.security.authentication.pat.PatAuthenticationManager;
|
||||
import run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher;
|
||||
import run.halo.app.security.authentication.twofactor.TwoFactorAuthorizationManager;
|
||||
import run.halo.app.security.authorization.AuthorityUtils;
|
||||
import run.halo.app.security.authorization.RequestInfoAuthorizationManager;
|
||||
import run.halo.app.security.session.InMemoryReactiveIndexedSessionRepository;
|
||||
import run.halo.app.security.session.ReactiveIndexedSessionRepository;
|
||||
|
@ -63,8 +71,6 @@ public class WebServerSecurityConfig {
|
|||
var staticResourcesMatcher = pathMatchers(HttpMethod.GET,
|
||||
"/themes/{themeName}/assets/{*resourcePaths}",
|
||||
"/plugins/{pluginName}/assets/**",
|
||||
"/console/**",
|
||||
"/uc/**",
|
||||
"/upload/**",
|
||||
"/webjars/**",
|
||||
"/js/**",
|
||||
|
@ -80,15 +86,26 @@ public class WebServerSecurityConfig {
|
|||
.authorizeExchange(spec -> spec.pathMatchers(
|
||||
"/api/**",
|
||||
"/apis/**",
|
||||
"/oauth2/**",
|
||||
"/actuator/**"
|
||||
).access(new RequestInfoAuthorizationManager(roleService))
|
||||
.pathMatchers(
|
||||
"/login/**",
|
||||
"/challenges/**",
|
||||
"/password-reset/**",
|
||||
"/signup",
|
||||
"/logout"
|
||||
).permitAll()
|
||||
.pathMatchers("/console/**", "/uc/**").authenticated()
|
||||
.matchers(createHtmlMatcher()).access((authentication, context) ->
|
||||
// we only need to check the authentication is authenticated
|
||||
// because we treat anonymous user as authenticated
|
||||
authentication.map(Authentication::isAuthenticated)
|
||||
.map(AuthorizationDecision::new)
|
||||
.switchIfEmpty(Mono.fromSupplier(() -> new AuthorizationDecision(false)))
|
||||
)
|
||||
.access(new RequestInfoAuthorizationManager(roleService))
|
||||
.pathMatchers("/challenges/two-factor/**")
|
||||
.access(new TwoFactorAuthorizationManager())
|
||||
.anyExchange().permitAll())
|
||||
.anonymous(spec -> {
|
||||
spec.authorities(AnonymousUserConst.Role);
|
||||
spec.authorities(AuthorityUtils.ROLE_PREFIX + AnonymousUserConst.Role);
|
||||
spec.principal(AnonymousUserConst.PRINCIPAL);
|
||||
})
|
||||
.securityContextRepository(securityContextRepository)
|
||||
|
@ -158,4 +175,14 @@ public class WebServerSecurityConfig {
|
|||
return new RsaKeyService(haloProperties.getWorkDir().resolve("keys"));
|
||||
}
|
||||
|
||||
private static ServerWebExchangeMatcher createHtmlMatcher() {
|
||||
ServerWebExchangeMatcher get =
|
||||
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/**");
|
||||
ServerWebExchangeMatcher notFavicon = new NegatedServerWebExchangeMatcher(
|
||||
ServerWebExchangeMatchers.pathMatchers("/favicon.*"));
|
||||
MediaTypeServerWebExchangeMatcher html =
|
||||
new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML);
|
||||
html.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
|
||||
return new AndServerWebExchangeMatcher(get, notFavicon, html);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package run.halo.app.infra.exception;
|
||||
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import run.halo.app.core.extension.UserConnection;
|
||||
|
||||
/**
|
||||
* An exception that the user has been bound to another OAuth2 user.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
public class OAuth2UserAlreadyBoundException extends ServerWebInputException {
|
||||
|
||||
public OAuth2UserAlreadyBoundException(UserConnection connection) {
|
||||
super("The user has been bound to another account", null, null, null, new Object[] {
|
||||
connection.getSpec().getUsername(),
|
||||
connection.getSpec().getProviderUserId(),
|
||||
connection.getSpec().getRegistrationId(),
|
||||
connection.getSpec().getUpdatedAt()
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -1,21 +1,73 @@
|
|||
package run.halo.app.security;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler;
|
||||
import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;
|
||||
import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.AuthenticationConverterServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler;
|
||||
import org.springframework.security.web.server.authorization.ServerWebExchangeDelegatingServerAccessDeniedHandler;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.security.authentication.SecurityConfigurer;
|
||||
import run.halo.app.security.authentication.twofactor.TwoFactorAuthenticationEntryPoint;
|
||||
|
||||
@Component
|
||||
public class ExceptionSecurityConfigurer implements SecurityConfigurer {
|
||||
|
||||
private final MessageSource messageSource;
|
||||
|
||||
private final ServerResponse.Context context;
|
||||
|
||||
public ExceptionSecurityConfigurer(MessageSource messageSource,
|
||||
ServerResponse.Context context) {
|
||||
this.messageSource = messageSource;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(ServerHttpSecurity http) {
|
||||
http.exceptionHandling(exception -> {
|
||||
var accessDeniedHandler = new BearerTokenServerAccessDeniedHandler();
|
||||
var entryPoint = new DefaultServerAuthenticationEntryPoint();
|
||||
exception
|
||||
.authenticationEntryPoint(entryPoint)
|
||||
.accessDeniedHandler(accessDeniedHandler);
|
||||
var accessDeniedHandlers =
|
||||
new ArrayList<ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry>(
|
||||
2
|
||||
);
|
||||
accessDeniedHandlers.add(
|
||||
new ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry(
|
||||
new AuthenticationConverterServerWebExchangeMatcher(
|
||||
new ServerBearerTokenAuthenticationConverter()
|
||||
),
|
||||
new BearerTokenServerAccessDeniedHandler()
|
||||
));
|
||||
accessDeniedHandlers.add(
|
||||
new ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry(
|
||||
ServerWebExchangeMatchers.anyExchange(),
|
||||
new HttpStatusServerAccessDeniedHandler(HttpStatus.FORBIDDEN)
|
||||
)
|
||||
);
|
||||
|
||||
var entryPoints =
|
||||
new ArrayList<DelegatingServerAuthenticationEntryPoint.DelegateEntry>(3);
|
||||
entryPoints.add(new DelegatingServerAuthenticationEntryPoint.DelegateEntry(
|
||||
TwoFactorAuthenticationEntryPoint.MATCHER,
|
||||
new TwoFactorAuthenticationEntryPoint(messageSource, context)
|
||||
));
|
||||
entryPoints.add(new DelegatingServerAuthenticationEntryPoint.DelegateEntry(
|
||||
exchange -> ServerWebExchangeMatcher.MatchResult.match(),
|
||||
new DefaultServerAuthenticationEntryPoint()
|
||||
));
|
||||
|
||||
exception.authenticationEntryPoint(
|
||||
new DelegatingServerAuthenticationEntryPoint(entryPoints)
|
||||
)
|
||||
.accessDeniedHandler(
|
||||
new ServerWebExchangeDelegatingServerAccessDeniedHandler(accessDeniedHandlers)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.springframework.security.core.AuthenticationException;
|
|||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.security.authentication.oauth2.OAuth2LoginHandlerEnhancer;
|
||||
import run.halo.app.security.authentication.rememberme.RememberMeRequestCache;
|
||||
import run.halo.app.security.authentication.rememberme.RememberMeServices;
|
||||
import run.halo.app.security.authentication.rememberme.WebSessionRememberMeRequestCache;
|
||||
|
@ -29,12 +30,17 @@ public class LoginHandlerEnhancerImpl implements LoginHandlerEnhancer {
|
|||
private final RememberMeRequestCache rememberMeRequestCache =
|
||||
new WebSessionRememberMeRequestCache();
|
||||
|
||||
private final OAuth2LoginHandlerEnhancer oauth2LoginHandlerEnhancer;
|
||||
|
||||
@Override
|
||||
public Mono<Void> onLoginSuccess(ServerWebExchange exchange,
|
||||
Authentication successfulAuthentication) {
|
||||
return rememberMeServices.loginSuccess(exchange, successfulAuthentication)
|
||||
.then(deviceService.loginSuccess(exchange, successfulAuthentication))
|
||||
.then(rememberMeRequestCache.removeRememberMe(exchange));
|
||||
return Mono.when(
|
||||
rememberMeServices.loginSuccess(exchange, successfulAuthentication),
|
||||
deviceService.loginSuccess(exchange, successfulAuthentication),
|
||||
rememberMeRequestCache.removeRememberMe(exchange),
|
||||
oauth2LoginHandlerEnhancer.loginSuccess(exchange, successfulAuthentication)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -4,7 +4,9 @@ import static org.springframework.security.config.web.server.SecurityWebFiltersO
|
|||
import static org.springframework.security.config.web.server.SecurityWebFiltersOrder.AUTHENTICATION;
|
||||
import static org.springframework.security.config.web.server.SecurityWebFiltersOrder.FIRST;
|
||||
import static org.springframework.security.config.web.server.SecurityWebFiltersOrder.FORM_LOGIN;
|
||||
import static org.springframework.security.config.web.server.SecurityWebFiltersOrder.HTTP_BASIC;
|
||||
import static org.springframework.security.config.web.server.SecurityWebFiltersOrder.LAST;
|
||||
import static org.springframework.security.config.web.server.SecurityWebFiltersOrder.OAUTH2_AUTHORIZATION_CODE;
|
||||
|
||||
import lombok.Setter;
|
||||
import org.pf4j.ExtensionPoint;
|
||||
|
@ -35,6 +37,10 @@ public class SecurityWebFiltersConfigurer implements SecurityConfigurer {
|
|||
new SecurityWebFilterChainProxy(BeforeSecurityWebFilter.class),
|
||||
FIRST
|
||||
)
|
||||
.addFilterAt(
|
||||
new SecurityWebFilterChainProxy(HttpBasicSecurityWebFilter.class),
|
||||
HTTP_BASIC
|
||||
)
|
||||
.addFilterAt(
|
||||
new SecurityWebFilterChainProxy(FormLoginSecurityWebFilter.class),
|
||||
FORM_LOGIN
|
||||
|
@ -47,6 +53,10 @@ public class SecurityWebFiltersConfigurer implements SecurityConfigurer {
|
|||
new SecurityWebFilterChainProxy(AnonymousAuthenticationSecurityWebFilter.class),
|
||||
ANONYMOUS_AUTHENTICATION
|
||||
)
|
||||
.addFilterAt(
|
||||
new SecurityWebFilterChainProxy(OAuth2AuthorizationCodeSecurityWebFilter.class),
|
||||
OAUTH2_AUTHORIZATION_CODE
|
||||
)
|
||||
.addFilterAt(
|
||||
new SecurityWebFilterChainProxy(AfterSecurityWebFilter.class),
|
||||
LAST
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package run.halo.app.security.authentication.oauth2;
|
||||
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.user.service.UserConnectionService;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link OAuth2LoginHandlerEnhancer}.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DefaultOAuth2LoginHandlerEnhancer implements OAuth2LoginHandlerEnhancer {
|
||||
|
||||
private final UserConnectionService connectionService;
|
||||
|
||||
@Setter
|
||||
private OAuth2AuthenticationTokenCache oauth2TokenCache =
|
||||
new WebSessionOAuth2AuthenticationTokenCache();
|
||||
|
||||
private final AuthenticationTrustResolver authenticationTrustResolver =
|
||||
new AuthenticationTrustResolverImpl();
|
||||
|
||||
public DefaultOAuth2LoginHandlerEnhancer(UserConnectionService connectionService) {
|
||||
this.connectionService = connectionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> loginSuccess(ServerWebExchange exchange, Authentication authentication) {
|
||||
if (!authenticationTrustResolver.isFullyAuthenticated(authentication)) {
|
||||
// Should never happen
|
||||
// Remove token directly if not fully authenticated
|
||||
return oauth2TokenCache.removeToken(exchange).then();
|
||||
}
|
||||
return oauth2TokenCache.getToken(exchange)
|
||||
.flatMap(oauth2Token -> {
|
||||
var oauth2User = oauth2Token.getPrincipal();
|
||||
var username = authentication.getName();
|
||||
var registrationId = oauth2Token.getAuthorizedClientRegistrationId();
|
||||
return connectionService.updateUserConnectionIfPresent(registrationId, oauth2User)
|
||||
.doOnNext(connection -> {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(
|
||||
"User connection already exists, skip creating. connection: [{}]",
|
||||
connection
|
||||
);
|
||||
}
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> connectionService.createUserConnection(
|
||||
username,
|
||||
registrationId,
|
||||
oauth2User
|
||||
)))
|
||||
.then(oauth2TokenCache.removeToken(exchange));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package run.halo.app.security.authentication.oauth2;
|
||||
|
||||
import static run.halo.app.security.authentication.oauth2.HaloOAuth2AuthenticationToken.authenticated;
|
||||
|
||||
import java.net.URI;
|
||||
import lombok.Setter;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.ServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
|
||||
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.user.service.UserConnectionService;
|
||||
|
||||
/**
|
||||
* A filter to map OAuth2 authentication to authenticated user.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
class MapOAuth2AuthenticationFilter implements WebFilter {
|
||||
|
||||
private static final String PRE_AUTHENTICATION =
|
||||
MapOAuth2AuthenticationFilter.class.getName() + ".PRE_AUTHENTICATION";
|
||||
|
||||
private final UserConnectionService connectionService;
|
||||
|
||||
private final ServerSecurityContextRepository securityContextRepository;
|
||||
|
||||
@Setter
|
||||
private OAuth2AuthenticationTokenCache authenticationCache =
|
||||
new WebSessionOAuth2AuthenticationTokenCache();
|
||||
|
||||
private final ReactiveUserDetailsService userDetailsService;
|
||||
|
||||
private final ServerLogoutHandler logoutHandler;
|
||||
|
||||
private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
|
||||
|
||||
@Setter
|
||||
private AuthenticationTrustResolver authenticationTrustResolver
|
||||
= new AuthenticationTrustResolverImpl();
|
||||
|
||||
public MapOAuth2AuthenticationFilter(
|
||||
ServerSecurityContextRepository securityContextRepository,
|
||||
UserConnectionService connectionService,
|
||||
ReactiveUserDetailsService userDetailsService) {
|
||||
this.connectionService = connectionService;
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
this.userDetailsService = userDetailsService;
|
||||
var logoutHandler = new SecurityContextServerLogoutHandler();
|
||||
logoutHandler.setSecurityContextRepository(securityContextRepository);
|
||||
this.logoutHandler = logoutHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.filter(authenticationTrustResolver::isAuthenticated)
|
||||
.doOnNext(
|
||||
// cache the pre-authentication
|
||||
authentication -> exchange.getAttributes().put(PRE_AUTHENTICATION, authentication)
|
||||
)
|
||||
.then(chain.filter(exchange))
|
||||
.then(Mono.defer(() -> ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.filter(OAuth2AuthenticationToken.class::isInstance)
|
||||
.cast(OAuth2AuthenticationToken.class)
|
||||
.flatMap(oauth2Token -> {
|
||||
var registrationId = oauth2Token.getAuthorizedClientRegistrationId();
|
||||
var oauth2User = oauth2Token.getPrincipal();
|
||||
// check the connection
|
||||
return connectionService.updateUserConnectionIfPresent(
|
||||
registrationId, oauth2User
|
||||
)
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
var preAuthenticationObject = exchange.getAttribute(PRE_AUTHENTICATION);
|
||||
if (preAuthenticationObject instanceof Authentication preAuth
|
||||
&& authenticationTrustResolver.isAuthenticated(preAuth)) {
|
||||
// check the authentication again
|
||||
// try to bind the user automatically
|
||||
return connectionService.createUserConnection(
|
||||
preAuth.getName(), registrationId, oauth2User
|
||||
);
|
||||
}
|
||||
// save the OAuth2Authentication into session
|
||||
return authenticationCache.saveToken(exchange, oauth2Token)
|
||||
.then(Mono.defer(() -> {
|
||||
var webFilterExchange = new WebFilterExchange(exchange, chain);
|
||||
// clear the security context
|
||||
return logoutHandler.logout(webFilterExchange, oauth2Token);
|
||||
}))
|
||||
.then(Mono.defer(() -> redirectStrategy.sendRedirect(exchange,
|
||||
URI.create("/login?oauth2_bind")
|
||||
)))
|
||||
// skip handling
|
||||
.then(Mono.empty());
|
||||
}))
|
||||
// user bound and remap the authentication
|
||||
.flatMap(connection ->
|
||||
userDetailsService.findByUsername(connection.getSpec().getUsername())
|
||||
)
|
||||
.map(userDetails -> authenticated(userDetails, oauth2Token))
|
||||
.flatMap(haloOAuth2Token -> {
|
||||
var securityContext = new SecurityContextImpl(haloOAuth2Token);
|
||||
return securityContextRepository.save(exchange, securityContext);
|
||||
// because this happens after the filter, there is no need to
|
||||
// write SecurityContext to the context
|
||||
});
|
||||
})
|
||||
.then())
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package run.halo.app.security.authentication.oauth2;
|
||||
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* OAuth2 authentication token cache. Saving OAuth2AuthenticationToken is mainly for further binding
|
||||
* to Halo user.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
public interface OAuth2AuthenticationTokenCache {
|
||||
|
||||
/**
|
||||
* Save OAuth2AuthenticationToken into cache.
|
||||
*
|
||||
* @param exchange Server web exchange
|
||||
* @param oauth2Token OAuth2AuthenticationToken
|
||||
* @return empty
|
||||
*/
|
||||
Mono<Void> saveToken(ServerWebExchange exchange, OAuth2AuthenticationToken oauth2Token);
|
||||
|
||||
/**
|
||||
* Get OAuth2AuthenticationToken from cache.
|
||||
*
|
||||
* @param exchange Server web exchange
|
||||
* @return an {@link OAuth2AuthenticationToken} if present, empty otherwise
|
||||
*/
|
||||
Mono<OAuth2AuthenticationToken> getToken(ServerWebExchange exchange);
|
||||
|
||||
/**
|
||||
* Remove OAuth2AuthenticationToken from cache.
|
||||
*
|
||||
* @param exchange Server web exchange
|
||||
* @return empty
|
||||
*/
|
||||
Mono<Void> removeToken(ServerWebExchange exchange);
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package run.halo.app.security.authentication.oauth2;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* OAuth2 login handler enhancer.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
public interface OAuth2LoginHandlerEnhancer {
|
||||
|
||||
Mono<Void> loginSuccess(ServerWebExchange exchange, Authentication authentication);
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package run.halo.app.security.authentication.oauth2;
|
||||
|
||||
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.user.service.UserConnectionService;
|
||||
import run.halo.app.security.authentication.SecurityConfigurer;
|
||||
|
||||
/**
|
||||
* OAuth2 security configurer.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
@Component
|
||||
class OAuth2SecurityConfigurer implements SecurityConfigurer {
|
||||
|
||||
private final ServerSecurityContextRepository securityContextRepository;
|
||||
|
||||
private final UserConnectionService connectionService;
|
||||
|
||||
private final ReactiveUserDetailsService userDetailsService;
|
||||
|
||||
public OAuth2SecurityConfigurer(ServerSecurityContextRepository securityContextRepository,
|
||||
UserConnectionService connectionService, ReactiveUserDetailsService userDetailsService) {
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
this.connectionService = connectionService;
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(ServerHttpSecurity http) {
|
||||
var mapOAuth2Filter = new MapOAuth2AuthenticationFilter(
|
||||
securityContextRepository, connectionService, userDetailsService
|
||||
);
|
||||
http.addFilterBefore(mapOAuth2Filter, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package run.halo.app.security.authentication.oauth2;
|
||||
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* WebSession cache implementation of {@link OAuth2AuthenticationTokenCache}.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
public class WebSessionOAuth2AuthenticationTokenCache implements OAuth2AuthenticationTokenCache {
|
||||
|
||||
private static final String SESSION_ATTRIBUTE_KEY =
|
||||
OAuth2AuthenticationTokenCache.class + ".OAUTH2_TOKEN";
|
||||
|
||||
@Override
|
||||
public Mono<Void> saveToken(ServerWebExchange exchange, OAuth2AuthenticationToken oauth2Token) {
|
||||
return exchange.getSession()
|
||||
.doOnNext(session -> {
|
||||
session.getAttributes().put(SESSION_ATTRIBUTE_KEY, oauth2Token);
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<OAuth2AuthenticationToken> getToken(ServerWebExchange exchange) {
|
||||
return exchange.getSession()
|
||||
.mapNotNull(session -> session.getAttribute(SESSION_ATTRIBUTE_KEY))
|
||||
.filter(OAuth2AuthenticationToken.class::isInstance)
|
||||
.cast(OAuth2AuthenticationToken.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeToken(ServerWebExchange exchange) {
|
||||
return exchange.getSession()
|
||||
.doOnNext(session -> session.getAttributes().remove(SESSION_ATTRIBUTE_KEY))
|
||||
.then();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package run.halo.app.security.authentication.twofactor;
|
||||
|
||||
import java.net.URI;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.exception.Exceptions;
|
||||
|
||||
public class TwoFactorAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
|
||||
|
||||
public static ServerWebExchangeMatcher MATCHER = exchange -> exchange.getPrincipal()
|
||||
.filter(TwoFactorAuthentication.class::isInstance)
|
||||
.flatMap(a -> ServerWebExchangeMatcher.MatchResult.match())
|
||||
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
|
||||
|
||||
private static final String REDIRECT_LOCATION = "/challenges/two-factor/totp";
|
||||
|
||||
private final RedirectServerAuthenticationEntryPoint redirectEntryPoint =
|
||||
new RedirectServerAuthenticationEntryPoint(REDIRECT_LOCATION);
|
||||
|
||||
private final MessageSource messageSource;
|
||||
|
||||
private final ServerResponse.Context context;
|
||||
|
||||
private static final ServerWebExchangeMatcher XHR_MATCHER = exchange -> {
|
||||
if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With")
|
||||
.contains("XMLHttpRequest")) {
|
||||
return ServerWebExchangeMatcher.MatchResult.match();
|
||||
}
|
||||
return ServerWebExchangeMatcher.MatchResult.notMatch();
|
||||
};
|
||||
|
||||
public TwoFactorAuthenticationEntryPoint(MessageSource messageSource,
|
||||
ServerResponse.Context context) {
|
||||
this.messageSource = messageSource;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
|
||||
return XHR_MATCHER.matches(exchange)
|
||||
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
|
||||
.switchIfEmpty(redirectEntryPoint.commence(exchange, ex).then(Mono.empty()))
|
||||
.flatMap(isXhr -> {
|
||||
var errorResponse = Exceptions.createErrorResponse(
|
||||
new TwoFactorAuthRequiredException(URI.create(REDIRECT_LOCATION)),
|
||||
null, exchange, messageSource);
|
||||
return ServerResponse.status(errorResponse.getStatusCode())
|
||||
.bodyValue(errorResponse.getBody())
|
||||
.flatMap(response -> response.writeTo(exchange, context));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package run.halo.app.security.authentication.twofactor;
|
||||
|
||||
import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.ReactiveAuthorizationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.server.authorization.AuthorizationContext;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class TwoFactorAuthorizationManager
|
||||
implements ReactiveAuthorizationManager<AuthorizationContext> {
|
||||
|
||||
@Override
|
||||
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication,
|
||||
AuthorizationContext context) {
|
||||
return authentication.map(TwoFactorAuthentication.class::isInstance)
|
||||
.defaultIfEmpty(false)
|
||||
.map(AuthorizationDecision::new);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package run.halo.app.security.authorization;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* Attributes is used by an Authorizer to get information about a request
|
||||
* that is used to make an authorization decision.
|
||||
|
@ -10,10 +8,6 @@ import java.security.Principal;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
public interface Attributes {
|
||||
/**
|
||||
* @return the UserDetails object to authorize
|
||||
*/
|
||||
Principal getPrincipal();
|
||||
|
||||
/**
|
||||
* @return the verb associated with API requests(this includes get, list,
|
||||
|
|
|
@ -1,23 +1,14 @@
|
|||
package run.halo.app.security.authorization;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class AttributesRecord implements Attributes {
|
||||
private final RequestInfo requestInfo;
|
||||
private final Principal principal;
|
||||
|
||||
public AttributesRecord(Principal principal, RequestInfo requestInfo) {
|
||||
public AttributesRecord(RequestInfo requestInfo) {
|
||||
this.requestInfo = requestInfo;
|
||||
this.principal = principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Principal getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -39,8 +39,8 @@ public enum AuthorityUtils {
|
|||
Collection<? extends GrantedAuthority> authorities) {
|
||||
return authorities.stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.filter(authority -> StringUtils.startsWith(authority, ROLE_PREFIX))
|
||||
.map(authority -> {
|
||||
authority = StringUtils.removeStart(authority, SCOPE_PREFIX);
|
||||
authority = StringUtils.removeStart(authority, ROLE_PREFIX);
|
||||
return authority;
|
||||
})
|
||||
|
|
|
@ -27,7 +27,7 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
|||
public Mono<AuthorizingVisitor> visitRules(Authentication authentication,
|
||||
RequestInfo requestInfo) {
|
||||
var roleNames = AuthorityUtils.authoritiesToRoles(authentication.getAuthorities());
|
||||
var record = new AttributesRecord(authentication, requestInfo);
|
||||
var record = new AttributesRecord(requestInfo);
|
||||
var visitor = new AuthorizingVisitor(record);
|
||||
|
||||
// If the request is an userspace scoped request,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
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.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import run.halo.app.security.authentication.oauth2.HaloOAuth2AuthenticationToken;
|
||||
|
||||
/**
|
||||
* Mixin for {@link HaloOAuth2AuthenticationToken}.
|
||||
*
|
||||
* @author johnniang
|
||||
* @since 2.20.0
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,
|
||||
getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
abstract class HaloOAuth2AuthenticationTokenMixin {
|
||||
|
||||
@JsonCreator
|
||||
HaloOAuth2AuthenticationTokenMixin(
|
||||
@JsonProperty("userDetails") UserDetails userDetails,
|
||||
@JsonProperty("original") OAuth2AuthenticationToken original
|
||||
) {
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ 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.oauth2.HaloOAuth2AuthenticationToken;
|
||||
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
||||
|
||||
/**
|
||||
|
@ -21,8 +22,12 @@ public class HaloSecurityJackson2Module extends SimpleModule {
|
|||
public void setupModule(SetupContext context) {
|
||||
SecurityJackson2Modules.enableDefaultTyping(context.getOwner());
|
||||
context.setMixInAnnotations(HaloUser.class, HaloUserMixin.class);
|
||||
context.setMixInAnnotations(TwoFactorAuthentication.class,
|
||||
TwoFactorAuthenticationMixin.class);
|
||||
context.setMixInAnnotations(
|
||||
TwoFactorAuthentication.class, TwoFactorAuthenticationMixin.class
|
||||
);
|
||||
context.setMixInAnnotations(
|
||||
HaloOAuth2AuthenticationToken.class, HaloOAuth2AuthenticationTokenMixin.class
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ import org.springframework.security.core.Authentication;
|
|||
abstract class TwoFactorAuthenticationMixin {
|
||||
|
||||
@JsonCreator
|
||||
TwoFactorAuthenticationMixin(@JsonProperty("previous") Authentication previous) {
|
||||
TwoFactorAuthenticationMixin(
|
||||
@JsonProperty("previous") Authentication previous
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
|
||||
import org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils;
|
||||
import org.thymeleaf.extras.springsecurity6.util.SpringVersionUtils;
|
||||
import run.halo.app.security.authorization.AuthorityUtils;
|
||||
|
||||
/**
|
||||
* HaloSpringSecurityDialect overwrites value of thymeleafSpringSecurityContext.
|
||||
|
@ -40,7 +41,9 @@ public class HaloSpringSecurityDialect extends SpringSecurityDialect implements
|
|||
// We have to build an anonymous authentication token here because the token won't be saved
|
||||
// into repository during anonymous authentication.
|
||||
var anonymousAuthentication =
|
||||
new AnonymousAuthenticationToken("fallback", PRINCIPAL, createAuthorityList(Role));
|
||||
new AnonymousAuthenticationToken(
|
||||
"fallback", PRINCIPAL, createAuthorityList(AuthorityUtils.ROLE_PREFIX + Role)
|
||||
);
|
||||
var anonymousSecurityContext = new SecurityContextImpl(anonymousAuthentication);
|
||||
|
||||
final Function<ServerWebExchange, Object> secCtxInitializer =
|
||||
|
|
|
@ -30,6 +30,7 @@ problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFo
|
|||
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Wrong Dependency Version
|
||||
problemDetail.title.run.halo.app.infra.exception.PluginDependentsNotDisabledException=Dependents Not Disabled
|
||||
problemDetail.title.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=Dependencies Not Enabled
|
||||
problemDetail.title.run.halo.app.infra.exception.OAuth2UserAlreadyBoundException=User Already Bound Error
|
||||
problemDetail.title.internalServerError=Internal Server Error
|
||||
problemDetail.title.conflict=Conflict
|
||||
|
||||
|
@ -55,6 +56,7 @@ problemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundExc
|
|||
problemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Dependencies have wrong version: {0}.
|
||||
problemDetail.run.halo.app.infra.exception.PluginDependentsNotDisabledException=Plugin dependents {0} are not fully disabled, please disable them first.
|
||||
problemDetail.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=Plugin dependencies {0} are not fully enabled, please enable them first.
|
||||
problemDetail.run.halo.app.infra.exception.OAuth2UserAlreadyBoundException=The user {0} has already been bound to another OAuth2 user, cannot automatically bind the current OAuth2 user.
|
||||
|
||||
problemDetail.index.duplicateKey=The value of {0} already exists for unique index {1}, please rename it and retry.
|
||||
problemDetail.user.email.verify.maxAttempts=Too many verification attempts, please try again later.
|
||||
|
|
|
@ -18,6 +18,8 @@ problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFo
|
|||
problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本错误
|
||||
problemDetail.title.run.halo.app.infra.exception.PluginDependentsNotDisabledException=子插件未禁用
|
||||
problemDetail.title.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=依赖未启用
|
||||
problemDetail.title.run.halo.app.infra.exception.OAuth2UserAlreadyBoundException=用户已绑定错误
|
||||
|
||||
problemDetail.title.internalServerError=服务器内部错误
|
||||
problemDetail.title.conflict=冲突
|
||||
|
||||
|
@ -32,6 +34,7 @@ problemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundExc
|
|||
problemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本有误:{0}。
|
||||
problemDetail.run.halo.app.infra.exception.PluginDependentsNotDisabledException=子插件 {0} 未完全禁用,请先禁用它们。
|
||||
problemDetail.run.halo.app.infra.exception.PluginDependenciesNotEnabledException=插件依赖 {0} 未完全启用,请先启用它们。
|
||||
problemDetail.run.halo.app.infra.exception.OAuth2UserAlreadyBoundException=用户 {0} 已经绑定到另一个 OAuth2 用户,无法自动绑定当前 OAuth2 用户。
|
||||
|
||||
problemDetail.index.duplicateKey=唯一索引 {1} 中的值 {0} 已存在,请更名后重试。
|
||||
problemDetail.user.email.verify.maxAttempts=尝试次数过多,请稍候再试。
|
||||
|
|
|
@ -19,7 +19,10 @@
|
|||
<strong th:text="#{messages.logoutSuccess}"></strong>
|
||||
</div>
|
||||
<div class="alert" role="alert" th:if="${param.signup.size() > 0}">
|
||||
<strong th:text="#{messages.signupSuccess}"> </strong>
|
||||
<strong th:text="#{messages.signupSuccess}"></strong>
|
||||
</div>
|
||||
<div class="alert" role="alert" th:if="${param.oauth2_bind.size() > 0}">
|
||||
<strong th:text="#{messages.oauth2_bind}"></strong>
|
||||
</div>
|
||||
|
||||
<div th:replace="~{__${fragmentTemplateName}__::form}"></div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
messages.loginError=无效的凭证。
|
||||
messages.logoutSuccess=登出成功。
|
||||
messages.signupSuccess=恭喜!注册成功,请立即登录。
|
||||
messages.oauth2_bind=当前登录未绑定账号,请尝试通过其他方式登录,登录成功后会自动绑定账号。
|
||||
|
||||
error.invalid-credential=无效的凭证。
|
||||
error.rate-limit-exceeded=请求过于频繁,请稍后再试。
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
messages.loginError=Invalid credentials.
|
||||
messages.logoutSuccess=Logout successfully.
|
||||
messages.oauth2_bind=The current login is not bound to an account. Please try to log in through other methods. After successful login, the account will be automatically bound.
|
||||
messages.signupSuccess=Congratulations! Sign up successfully, please login now.
|
||||
|
||||
error.invalid-credential=Invalid credentials.
|
||||
|
|
|
@ -10,6 +10,8 @@ import java.util.Set;
|
|||
import org.hamcrest.core.StringStartsWith;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
@ -17,6 +19,7 @@ import org.springframework.boot.test.context.TestConfiguration;
|
|||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.web.filter.reactive.ServerWebExchangeContextFilter;
|
||||
|
@ -121,23 +124,32 @@ class WebFluxConfigTest {
|
|||
@Nested
|
||||
class ConsoleRequest {
|
||||
|
||||
@Test
|
||||
void shouldRequestConsoleIndex() {
|
||||
List.of(
|
||||
"/console",
|
||||
"/console/index",
|
||||
"/console/index.html",
|
||||
"/console/dashboard",
|
||||
"/console/fake"
|
||||
)
|
||||
.forEach(uri -> webClient.get().uri(uri)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).value(StringStartsWith.startsWith("console index"))
|
||||
);
|
||||
@WithMockUser
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"/console",
|
||||
"/console/index",
|
||||
"/console/index.html",
|
||||
"/console/dashboard",
|
||||
"/console/fake"
|
||||
})
|
||||
void shouldRequestConsoleIndex(String uri) {
|
||||
webClient.get().uri(uri)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(String.class).value(StringStartsWith.startsWith("console index"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRedirectToLoginPageIfUnauthenticated() {
|
||||
webClient.get().uri("/console")
|
||||
.exchange()
|
||||
.expectStatus().isFound()
|
||||
.expectHeader().location("/login?authentication_required");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void shouldRequestConsoleAssetsCorrectly() {
|
||||
webClient.get().uri("/console/assets/fake.txt")
|
||||
.exchange()
|
||||
|
@ -146,6 +158,7 @@ class WebFluxConfigTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void shouldResponseNotFoundWhenAssetsNotExist() {
|
||||
webClient.get().uri("/console/assets/not-found.txt")
|
||||
.exchange()
|
||||
|
|
|
@ -29,7 +29,7 @@ class AuthorityUtilsTest {
|
|||
|
||||
var roles = authoritiesToRoles(authorities);
|
||||
|
||||
assertEquals(Set.of("admin", "owner", "manager", "faker", "system:read"), roles);
|
||||
assertEquals(Set.of("admin", "owner", "manager"), roles);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -37,7 +37,7 @@ class DefaultRuleResolverTest {
|
|||
void visitRules() {
|
||||
when(roleService.listDependenciesFlux(Set.of("ruleReadPost")))
|
||||
.thenReturn(Flux.just(mockRole()));
|
||||
var fakeUser = new User("admin", "123456", createAuthorityList("ruleReadPost"));
|
||||
var fakeUser = new User("admin", "123456", createAuthorityList("ROLE_ruleReadPost"));
|
||||
var authentication = authenticated(fakeUser, fakeUser.getPassword(),
|
||||
fakeUser.getAuthorities());
|
||||
|
||||
|
@ -59,7 +59,7 @@ class DefaultRuleResolverTest {
|
|||
void visitRulesForUserspaceScope() {
|
||||
when(roleService.listDependenciesFlux(Set.of("ruleReadPost")))
|
||||
.thenReturn(Flux.just(mockRole()));
|
||||
var fakeUser = new User("admin", "123456", createAuthorityList("ruleReadPost"));
|
||||
var fakeUser = new User("admin", "123456", createAuthorityList("ROLE_ruleReadPost"));
|
||||
var authentication =
|
||||
authenticated(fakeUser, fakeUser.getPassword(), fakeUser.getAuthorities());
|
||||
var cases = List.of(
|
||||
|
|
|
@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -14,7 +16,10 @@ 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 org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||
import run.halo.app.security.authentication.login.HaloUser;
|
||||
import run.halo.app.security.authentication.oauth2.HaloOAuth2AuthenticationToken;
|
||||
import run.halo.app.security.authentication.twofactor.TwoFactorAuthentication;
|
||||
|
||||
class HaloSecurityJacksonModuleTest {
|
||||
|
@ -39,10 +44,21 @@ class HaloSecurityJacksonModuleTest {
|
|||
|
||||
@Test
|
||||
void codecTwoFactorAuthenticationTokenTest() throws JsonProcessingException {
|
||||
codecAssert(haloUser -> new TwoFactorAuthentication(
|
||||
UsernamePasswordAuthenticationToken.authenticated(haloUser,
|
||||
codecAssert(haloUser -> {
|
||||
var authentication = UsernamePasswordAuthenticationToken.authenticated(haloUser,
|
||||
haloUser.getPassword(),
|
||||
haloUser.getAuthorities())));
|
||||
haloUser.getAuthorities());
|
||||
return new TwoFactorAuthentication(authentication);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void codecHaloOAuth2AuthenticationTokenTest() throws JsonProcessingException {
|
||||
codecAssert(haloUser -> {
|
||||
var oauth2User = new DefaultOAuth2User(List.of(), Map.of("name", "halo"), "name");
|
||||
var oauth2Token = new OAuth2AuthenticationToken(oauth2User, List.of(), "github");
|
||||
return new HaloOAuth2AuthenticationToken(haloUser, oauth2Token);
|
||||
});
|
||||
}
|
||||
|
||||
void codecAssert(Function<HaloUser, Authentication> authenticationConverter)
|
||||
|
|
Loading…
Reference in New Issue