mirror of https://github.com/halo-dev/halo
Refactor PAT authentication by making it standalone (#6878)
#### What type of PR is this? /kind improvement /area core /milestone 2.20.x #### What this PR does / why we need it: This PR makes PAT configuration standalone and removes unused configuration related with `JWT`. After this, we can define additional authentications in plugins with correct configuration order. #### Does this PR introduce a user-facing change? ```release-note None ```pull/6885/head
parent
db4e68b732
commit
514a05552f
|
@ -1,6 +1,5 @@
|
||||||
package run.halo.app.infra.config;
|
package run.halo.app.infra.config;
|
||||||
|
|
||||||
import static org.springframework.security.web.server.authentication.ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.builder;
|
|
||||||
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
|
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
|
||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
@ -33,8 +32,6 @@ import run.halo.app.security.HaloServerRequestCache;
|
||||||
import run.halo.app.security.authentication.CryptoService;
|
import run.halo.app.security.authentication.CryptoService;
|
||||||
import run.halo.app.security.authentication.SecurityConfigurer;
|
import run.halo.app.security.authentication.SecurityConfigurer;
|
||||||
import run.halo.app.security.authentication.impl.RsaKeyService;
|
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.authorization.AuthorityUtils;
|
import run.halo.app.security.authorization.AuthorityUtils;
|
||||||
import run.halo.app.security.session.InMemoryReactiveIndexedSessionRepository;
|
import run.halo.app.security.session.InMemoryReactiveIndexedSessionRepository;
|
||||||
import run.halo.app.security.session.ReactiveIndexedSessionRepository;
|
import run.halo.app.security.session.ReactiveIndexedSessionRepository;
|
||||||
|
@ -86,15 +83,6 @@ public class WebServerSecurityConfig {
|
||||||
basic.disable();
|
basic.disable();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.oauth2ResourceServer(oauth2 -> {
|
|
||||||
var authManagerResolver = builder().add(
|
|
||||||
new PatServerWebExchangeMatcher(),
|
|
||||||
new PatAuthenticationManager(client, cryptoService)
|
|
||||||
)
|
|
||||||
// TODO Add other authentication mangers here. e.g.: JwtAuthenticationManager.
|
|
||||||
.build();
|
|
||||||
oauth2.authenticationManagerResolver(authManagerResolver);
|
|
||||||
})
|
|
||||||
.headers(headerSpec -> headerSpec
|
.headers(headerSpec -> headerSpec
|
||||||
.frameOptions(frameSpec -> {
|
.frameOptions(frameSpec -> {
|
||||||
var frameOptions = haloProperties.getSecurity().getFrameOptions();
|
var frameOptions = haloProperties.getSecurity().getFrameOptions();
|
||||||
|
|
|
@ -11,6 +11,7 @@ import static org.springframework.security.config.web.server.SecurityWebFiltersO
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import org.pf4j.ExtensionPoint;
|
import org.pf4j.ExtensionPoint;
|
||||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
import org.springframework.security.web.server.WebFilterChainProxy;
|
import org.springframework.security.web.server.WebFilterChainProxy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
@ -22,6 +23,8 @@ import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||||
import run.halo.app.security.authentication.SecurityConfigurer;
|
import run.halo.app.security.authentication.SecurityConfigurer;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
// Specific an order here to control the order or security configurer initialization
|
||||||
|
@Order(-100)
|
||||||
public class SecurityWebFiltersConfigurer implements SecurityConfigurer {
|
public class SecurityWebFiltersConfigurer implements SecurityConfigurer {
|
||||||
|
|
||||||
private final ExtensionGetter extensionGetter;
|
private final ExtensionGetter extensionGetter;
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
package run.halo.app.security.authentication.jwt;
|
|
||||||
|
|
||||||
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
|
|
||||||
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
|
||||||
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.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
|
||||||
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
|
||||||
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
|
|
||||||
import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher;
|
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
|
||||||
import run.halo.app.infra.properties.JwtProperties;
|
|
||||||
import run.halo.app.security.authentication.SecurityConfigurer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Use It after 2.0.0.
|
|
||||||
*/
|
|
||||||
public class JwtAuthenticationConfigurer implements SecurityConfigurer {
|
|
||||||
|
|
||||||
private final ReactiveUserDetailsService userDetailsService;
|
|
||||||
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
|
|
||||||
private final ServerCodecConfigurer codec;
|
|
||||||
|
|
||||||
private final JwtEncoder jwtEncoder;
|
|
||||||
|
|
||||||
private final ServerResponse.Context context;
|
|
||||||
|
|
||||||
private final JwtProperties jwtProp;
|
|
||||||
|
|
||||||
public JwtAuthenticationConfigurer(ReactiveUserDetailsService userDetailsService,
|
|
||||||
PasswordEncoder passwordEncoder,
|
|
||||||
ServerCodecConfigurer codec,
|
|
||||||
JwtEncoder jwtEncoder,
|
|
||||||
ServerResponse.Context context,
|
|
||||||
JwtProperties jwtProp) {
|
|
||||||
this.userDetailsService = userDetailsService;
|
|
||||||
this.passwordEncoder = passwordEncoder;
|
|
||||||
this.codec = codec;
|
|
||||||
this.jwtEncoder = jwtEncoder;
|
|
||||||
this.context = context;
|
|
||||||
this.jwtProp = jwtProp;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void configure(ServerHttpSecurity http) {
|
|
||||||
var loginManager = new LoginAuthenticationManager(userDetailsService, passwordEncoder);
|
|
||||||
|
|
||||||
var filter = new AuthenticationWebFilter(loginManager);
|
|
||||||
var loginMatcher = new AndServerWebExchangeMatcher(
|
|
||||||
pathMatchers(HttpMethod.POST, "/api/auth/token"),
|
|
||||||
new MediaTypeServerWebExchangeMatcher(MediaType.APPLICATION_JSON)
|
|
||||||
);
|
|
||||||
|
|
||||||
filter.setRequiresAuthenticationMatcher(loginMatcher);
|
|
||||||
filter.setServerAuthenticationConverter(
|
|
||||||
new LoginAuthenticationConverter(codec.getReaders()));
|
|
||||||
filter.setAuthenticationSuccessHandler(
|
|
||||||
new LoginAuthenticationSuccessHandler(jwtEncoder, jwtProp, context));
|
|
||||||
filter.setAuthenticationFailureHandler(new LoginAuthenticationFailureHandler(context));
|
|
||||||
|
|
||||||
http.addFilterAt(filter, SecurityWebFiltersOrder.FORM_LOGIN);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
package run.halo.app.security.authentication.jwt;
|
|
||||||
|
|
||||||
import static org.springframework.security.authentication.UsernamePasswordAuthenticationToken.unauthenticated;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.http.codec.HttpMessageReader;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
public class LoginAuthenticationConverter implements ServerAuthenticationConverter {
|
|
||||||
|
|
||||||
private final List<HttpMessageReader<?>> reader;
|
|
||||||
|
|
||||||
public LoginAuthenticationConverter(List<HttpMessageReader<?>> reader) {
|
|
||||||
this.reader = reader;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<Authentication> convert(ServerWebExchange exchange) {
|
|
||||||
return ServerRequest.create(exchange, this.reader)
|
|
||||||
.bodyToMono(UsernamePasswordRequest.class)
|
|
||||||
.map(request -> unauthenticated(request.getUsername(), request.getPassword()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class UsernamePasswordRequest {
|
|
||||||
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package run.halo.app.security.authentication.jwt;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.security.core.AuthenticationException;
|
|
||||||
import org.springframework.security.web.server.WebFilterExchange;
|
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
public class LoginAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
|
|
||||||
|
|
||||||
private final ServerResponse.Context context;
|
|
||||||
|
|
||||||
public LoginAuthenticationFailureHandler(ServerResponse.Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange,
|
|
||||||
AuthenticationException exception) {
|
|
||||||
return ServerResponse.badRequest()
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.bodyValue(
|
|
||||||
Map.of("error", exception.getLocalizedMessage())
|
|
||||||
)
|
|
||||||
.flatMap(serverResponse ->
|
|
||||||
serverResponse.writeTo(webFilterExchange.getExchange(), context));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package run.halo.app.security.authentication.jwt;
|
|
||||||
|
|
||||||
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
|
|
||||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
|
|
||||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
|
|
||||||
public final class LoginAuthenticationManager
|
|
||||||
extends UserDetailsRepositoryReactiveAuthenticationManager {
|
|
||||||
|
|
||||||
public LoginAuthenticationManager(ReactiveUserDetailsService userDetailsService,
|
|
||||||
PasswordEncoder passwordEncoder) {
|
|
||||||
super(userDetailsService);
|
|
||||||
super.setPasswordEncoder(passwordEncoder);
|
|
||||||
if (userDetailsService instanceof ReactiveUserDetailsPasswordService passwordService) {
|
|
||||||
super.setUserDetailsPasswordService(passwordService);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
package run.halo.app.security.authentication.jwt;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
import org.springframework.security.oauth2.jwt.JwsHeader;
|
|
||||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
|
||||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
|
||||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
|
||||||
import org.springframework.security.web.server.WebFilterExchange;
|
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
import run.halo.app.infra.properties.JwtProperties;
|
|
||||||
|
|
||||||
public class LoginAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
|
|
||||||
|
|
||||||
private final JwtEncoder jwtEncoder;
|
|
||||||
|
|
||||||
private final JwtProperties jwtProp;
|
|
||||||
|
|
||||||
private final ServerResponse.Context context;
|
|
||||||
|
|
||||||
public LoginAuthenticationSuccessHandler(JwtEncoder jwtEncoder, JwtProperties jwtProp,
|
|
||||||
ServerResponse.Context context) {
|
|
||||||
this.jwtEncoder = jwtEncoder;
|
|
||||||
this.jwtProp = jwtProp;
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange,
|
|
||||||
Authentication authentication) {
|
|
||||||
var issuedAt = Instant.now();
|
|
||||||
// TODO Make the expiresAt configurable
|
|
||||||
var expiresAt = issuedAt.plus(24, ChronoUnit.HOURS);
|
|
||||||
var headers = JwsHeader.with(jwtProp.getJwsAlgorithm()).build();
|
|
||||||
var claims = JwtClaimsSet.builder()
|
|
||||||
.issuer("Halo Owner")
|
|
||||||
.issuedAt(issuedAt)
|
|
||||||
.expiresAt(expiresAt)
|
|
||||||
// the principal is the username
|
|
||||||
.subject(authentication.getName())
|
|
||||||
.claim("scope", authentication.getAuthorities().stream()
|
|
||||||
.map(GrantedAuthority::getAuthority)
|
|
||||||
.collect(Collectors.toList()))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
var jwt = jwtEncoder.encode(JwtEncoderParameters.from(headers, claims));
|
|
||||||
|
|
||||||
return ServerResponse.ok()
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.bodyValue(Map.of("token", jwt.getTokenValue()))
|
|
||||||
.flatMap(serverResponse -> serverResponse.writeTo(webFilterExchange.getExchange(),
|
|
||||||
this.context));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package run.halo.app.security.authentication.pat;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PAT authentication converter.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @since 2.20.4
|
||||||
|
*/
|
||||||
|
class PatAuthenticationConverter extends ServerBearerTokenAuthenticationConverter {
|
||||||
|
|
||||||
|
public static final String PAT_TOKEN_PREFIX = "pat_";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Authentication> convert(ServerWebExchange exchange) {
|
||||||
|
return super.convert(exchange)
|
||||||
|
.cast(BearerTokenAuthenticationToken.class)
|
||||||
|
.map(BearerTokenAuthenticationToken::getToken)
|
||||||
|
.filter(token -> StringUtils.startsWith(token, PAT_TOKEN_PREFIX))
|
||||||
|
.map(token -> StringUtils.removeStart(token, PAT_TOKEN_PREFIX))
|
||||||
|
.map(BearerTokenAuthenticationToken::new);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
package run.halo.app.security.authentication.pat;
|
package run.halo.app.security.authentication.pat;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.removeStart;
|
|
||||||
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSource;
|
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSource;
|
||||||
import static run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher.PAT_TOKEN_PREFIX;
|
|
||||||
import static run.halo.app.security.authorization.AuthorityUtils.ANONYMOUS_ROLE_NAME;
|
import static run.halo.app.security.authorization.AuthorityUtils.ANONYMOUS_ROLE_NAME;
|
||||||
import static run.halo.app.security.authorization.AuthorityUtils.AUTHENTICATED_ROLE_NAME;
|
import static run.halo.app.security.authorization.AuthorityUtils.AUTHENTICATED_ROLE_NAME;
|
||||||
import static run.halo.app.security.authorization.AuthorityUtils.ROLE_PREFIX;
|
import static run.halo.app.security.authorization.AuthorityUtils.ROLE_PREFIX;
|
||||||
|
@ -18,7 +16,6 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager
|
||||||
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;
|
||||||
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
|
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
|
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
@ -30,14 +27,14 @@ import run.halo.app.security.PersonalAccessToken;
|
||||||
import run.halo.app.security.authentication.CryptoService;
|
import run.halo.app.security.authentication.CryptoService;
|
||||||
import run.halo.app.security.authorization.AuthorityUtils;
|
import run.halo.app.security.authorization.AuthorityUtils;
|
||||||
|
|
||||||
public class PatAuthenticationManager implements ReactiveAuthenticationManager {
|
class PatAuthenticationManager implements ReactiveAuthenticationManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimal duration gap of personal access token update.
|
* Minimal duration gap of personal access token update.
|
||||||
*/
|
*/
|
||||||
private static final Duration MIN_UPDATE_GAP = Duration.ofMinutes(1);
|
private static final Duration MIN_UPDATE_GAP = Duration.ofMinutes(1);
|
||||||
|
|
||||||
private final ReactiveAuthenticationManager delegate;
|
private final JwtReactiveAuthenticationManager delegate;
|
||||||
|
|
||||||
private final ReactiveExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
|
@ -52,33 +49,28 @@ public class PatAuthenticationManager implements ReactiveAuthenticationManager {
|
||||||
this.clock = Clock.systemDefaultZone();
|
this.clock = Clock.systemDefaultZone();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ReactiveAuthenticationManager getDelegate() {
|
private JwtReactiveAuthenticationManager getDelegate() {
|
||||||
var jwtDecoder = withJwkSource(signedJWT -> Flux.just(cryptoService.getJwk()))
|
var jwtDecoder = withJwkSource(signedJWT -> Flux.just(cryptoService.getJwk()))
|
||||||
.build();
|
.build();
|
||||||
return new JwtReactiveAuthenticationManager(jwtDecoder);
|
return new JwtReactiveAuthenticationManager(jwtDecoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClock(Clock clock) {
|
/**
|
||||||
|
* Set new clock. Only for testing.
|
||||||
|
*
|
||||||
|
* @param clock new clock
|
||||||
|
*/
|
||||||
|
void setClock(Clock clock) {
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Authentication> authenticate(Authentication authentication) {
|
public Mono<Authentication> authenticate(Authentication authentication) {
|
||||||
return Mono.just(authentication)
|
return delegate.authenticate(authentication)
|
||||||
.map(this::clearPrefix)
|
|
||||||
.flatMap(delegate::authenticate)
|
|
||||||
.cast(JwtAuthenticationToken.class)
|
.cast(JwtAuthenticationToken.class)
|
||||||
.flatMap(this::checkAndRebuild);
|
.flatMap(this::checkAndRebuild);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Authentication clearPrefix(Authentication authentication) {
|
|
||||||
if (authentication instanceof BearerTokenAuthenticationToken bearerToken) {
|
|
||||||
var newToken = removeStart(bearerToken.getToken(), PAT_TOKEN_PREFIX);
|
|
||||||
return new BearerTokenAuthenticationToken(newToken);
|
|
||||||
}
|
|
||||||
return authentication;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<JwtAuthenticationToken> checkAndRebuild(JwtAuthenticationToken jat) {
|
private Mono<JwtAuthenticationToken> checkAndRebuild(JwtAuthenticationToken jat) {
|
||||||
var jwt = jat.getToken();
|
var jwt = jat.getToken();
|
||||||
var patName = jwt.getClaimAsString("pat_name");
|
var patName = jwt.getClaimAsString("pat_name");
|
||||||
|
|
|
@ -15,7 +15,7 @@ import run.halo.app.extension.GroupVersion;
|
||||||
import run.halo.app.security.PersonalAccessToken;
|
import run.halo.app.security.PersonalAccessToken;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class PatEndpoint implements CustomEndpoint {
|
class PatEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
private final UserScopedPatHandler patHandler;
|
private final UserScopedPatHandler patHandler;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package run.halo.app.security.authentication.pat;
|
||||||
|
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
|
import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
||||||
|
import org.springframework.security.web.server.authentication.ServerAuthenticationEntryPointFailureHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.security.authentication.CryptoService;
|
||||||
|
import run.halo.app.security.authentication.SecurityConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PAT security configurer.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @since 2.20.4
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
// Specific an order here to control the order or security configurer initialization
|
||||||
|
@Order(0)
|
||||||
|
class PatSecurityConfigurer implements SecurityConfigurer {
|
||||||
|
|
||||||
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
|
private final CryptoService cryptoService;
|
||||||
|
|
||||||
|
public PatSecurityConfigurer(ReactiveExtensionClient client, CryptoService cryptoService) {
|
||||||
|
this.client = client;
|
||||||
|
this.cryptoService = cryptoService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(ServerHttpSecurity http) {
|
||||||
|
var filter =
|
||||||
|
new AuthenticationWebFilter(new PatAuthenticationManager(client, cryptoService));
|
||||||
|
filter.setServerAuthenticationConverter(new PatAuthenticationConverter());
|
||||||
|
var entryPoint = new BearerTokenServerAuthenticationEntryPoint();
|
||||||
|
var failureHandler = new ServerAuthenticationEntryPointFailureHandler(entryPoint);
|
||||||
|
filter.setAuthenticationFailureHandler(failureHandler);
|
||||||
|
http.addFilterAt(filter, SecurityWebFiltersOrder.AUTHENTICATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,30 +0,0 @@
|
||||||
package run.halo.app.security.authentication.pat;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.security.core.AuthenticationException;
|
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
|
||||||
import org.springframework.security.oauth2.server.resource.web.server.authentication.ServerBearerTokenAuthenticationConverter;
|
|
||||||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
|
|
||||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
public class PatServerWebExchangeMatcher implements ServerWebExchangeMatcher {
|
|
||||||
|
|
||||||
public static final String PAT_TOKEN_PREFIX = "pat_";
|
|
||||||
|
|
||||||
private final ServerAuthenticationConverter authConverter =
|
|
||||||
new ServerBearerTokenAuthenticationConverter();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<MatchResult> matches(ServerWebExchange exchange) {
|
|
||||||
return authConverter.convert(exchange)
|
|
||||||
.filter(a -> a instanceof BearerTokenAuthenticationToken)
|
|
||||||
.cast(BearerTokenAuthenticationToken.class)
|
|
||||||
.map(BearerTokenAuthenticationToken::getToken)
|
|
||||||
.filter(tokenString -> StringUtils.startsWith(tokenString, PAT_TOKEN_PREFIX))
|
|
||||||
.flatMap(t -> MatchResult.match())
|
|
||||||
.onErrorResume(AuthenticationException.class, t -> MatchResult.notMatch())
|
|
||||||
.switchIfEmpty(Mono.defer(MatchResult::notMatch));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
package run.halo.app.security.authentication.pat.impl;
|
package run.halo.app.security.authentication.pat;
|
||||||
|
|
||||||
import static run.halo.app.extension.Comparators.compareCreationTimestamp;
|
import static run.halo.app.extension.Comparators.compareCreationTimestamp;
|
||||||
import static run.halo.app.security.authentication.pat.PatServerWebExchangeMatcher.PAT_TOKEN_PREFIX;
|
import static run.halo.app.security.authentication.pat.PatAuthenticationConverter.PAT_TOKEN_PREFIX;
|
||||||
|
|
||||||
import com.nimbusds.jose.jwk.JWKSet;
|
import com.nimbusds.jose.jwk.JWKSet;
|
||||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||||
|
@ -41,11 +41,10 @@ import run.halo.app.infra.exception.AccessDeniedException;
|
||||||
import run.halo.app.infra.exception.NotFoundException;
|
import run.halo.app.infra.exception.NotFoundException;
|
||||||
import run.halo.app.security.PersonalAccessToken;
|
import run.halo.app.security.PersonalAccessToken;
|
||||||
import run.halo.app.security.authentication.CryptoService;
|
import run.halo.app.security.authentication.CryptoService;
|
||||||
import run.halo.app.security.authentication.pat.UserScopedPatHandler;
|
|
||||||
import run.halo.app.security.authorization.AuthorityUtils;
|
import run.halo.app.security.authorization.AuthorityUtils;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
class UserScopedPatHandlerImpl implements UserScopedPatHandler {
|
||||||
|
|
||||||
private static final String ACCESS_TOKEN_ANNO_NAME = "security.halo.run/access-token";
|
private static final String ACCESS_TOKEN_ANNO_NAME = "security.halo.run/access-token";
|
||||||
|
|
Loading…
Reference in New Issue