Keep length of PAT stable (#5374)

#### What type of PR is this?

/kind improvement
/area core
/milestone 2.13.x

#### What this PR does / why we need it:

In fact, PAT is a JWT, which is very long. However, we put the claim `roles` into PAT, which will cause the length of PAT to increase as the `roles` information increases.

So, the current PR removes the claim `roles` from PAT, which ensures that the length of PAT becomes stable and we can update roles information for PAT at runtime.

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/5366

#### Does this PR introduce a user-facing change?

```release-note
避免个人令牌长度随着角色信息增长
```
pull/5302/head^2
John Niang 2024-02-20 10:48:08 +08:00 committed by GitHub
parent 27c98aec36
commit 333422a0d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 34 additions and 74 deletions

View File

@ -1,53 +0,0 @@
package run.halo.app.security.authentication.jwt;
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.ROLE_PREFIX;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import reactor.core.publisher.Flux;
/**
* GrantedAuthorities converter for SCOPE_ and ROLE_ prefixes.
*
* @author johnniang
*/
public class JwtScopesAndRolesGrantedAuthoritiesConverter
implements Converter<Jwt, Flux<GrantedAuthority>> {
private final Converter<Jwt, Collection<GrantedAuthority>> delegate;
public JwtScopesAndRolesGrantedAuthoritiesConverter() {
delegate = new JwtGrantedAuthoritiesConverter();
}
@Override
public Flux<GrantedAuthority> convert(Jwt jwt) {
var grantedAuthorities = new ArrayList<GrantedAuthority>();
// add default roles
grantedAuthorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + ANONYMOUS_ROLE_NAME));
grantedAuthorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + AUTHENTICATED_ROLE_NAME));
var delegateAuthorities = delegate.convert(jwt);
if (delegateAuthorities != null) {
grantedAuthorities.addAll(delegateAuthorities);
}
var roles = jwt.getClaimAsStringList("roles");
if (roles != null) {
roles.stream()
.map(role -> ROLE_PREFIX + role)
.map(SimpleGrantedAuthority::new)
.forEach(grantedAuthorities::add);
}
return Flux.fromIterable(grantedAuthorities);
}
}

View File

@ -3,27 +3,31 @@ 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 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.AUTHENTICATED_ROLE_NAME;
import static run.halo.app.security.authorization.AuthorityUtils.ROLE_PREFIX;
import com.nimbusds.jwt.JWTClaimNames;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Objects;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.JwtReactiveAuthenticationManager;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import run.halo.app.extension.ExtensionUtil;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.security.PersonalAccessToken;
import run.halo.app.security.authentication.jwt.JwtScopesAndRolesGrantedAuthoritiesConverter;
import run.halo.app.security.authorization.AuthorityUtils;
public class PatAuthenticationManager implements ReactiveAuthenticationManager {
@ -44,15 +48,10 @@ public class PatAuthenticationManager implements ReactiveAuthenticationManager {
this.clock = Clock.systemDefaultZone();
}
private ReactiveAuthenticationManager getDelegate(PatJwkSupplier jwkSupplier) {
private static ReactiveAuthenticationManager getDelegate(PatJwkSupplier jwkSupplier) {
var jwtDecoder = withJwkSource(signedJWT -> Flux.just(jwkSupplier.getJwk()))
.build();
var jwtAuthManager = new JwtReactiveAuthenticationManager(jwtDecoder);
var jwtAuthConverter = new ReactiveJwtAuthenticationConverter();
jwtAuthConverter.setJwtGrantedAuthoritiesConverter(
new JwtScopesAndRolesGrantedAuthoritiesConverter());
jwtAuthManager.setJwtAuthenticationConverter(jwtAuthConverter);
return jwtAuthManager;
return new JwtReactiveAuthenticationManager(jwtDecoder);
}
public void setClock(Clock clock) {
@ -61,10 +60,11 @@ public class PatAuthenticationManager implements ReactiveAuthenticationManager {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
return delegate.authenticate(clearPrefix(authentication))
.transformDeferred(auth -> auth.filter(a -> a instanceof JwtAuthenticationToken)
.cast(JwtAuthenticationToken.class)
.flatMap(jwtAuthToken -> checkAvailability(jwtAuthToken).thenReturn(jwtAuthToken)));
return Mono.just(authentication)
.map(this::clearPrefix)
.flatMap(delegate::authenticate)
.cast(JwtAuthenticationToken.class)
.flatMap(this::checkAndRebuild);
}
private Authentication clearPrefix(Authentication authentication) {
@ -75,18 +75,33 @@ public class PatAuthenticationManager implements ReactiveAuthenticationManager {
return authentication;
}
private Mono<Void> checkAvailability(JwtAuthenticationToken jwtAuthToken) {
var jwt = jwtAuthToken.getToken();
private Mono<JwtAuthenticationToken> checkAndRebuild(JwtAuthenticationToken jat) {
var jwt = jat.getToken();
var patName = jwt.getClaimAsString("pat_name");
var jwtId = jwt.getClaimAsString(JWTClaimNames.JWT_ID);
if (patName == null || jwtId == null) {
// Skip if the JWT token is not a PAT.
return Mono.empty();
// Not a valid PAT
return Mono.error(new InvalidBearerTokenException("Missing claim pat_name or jti"));
}
return client.fetch(PersonalAccessToken.class, patName)
.switchIfEmpty(
Mono.error(() -> new DisabledException("Personal access token has been deleted.")))
.flatMap(pat -> patChecks(pat, jwtId).and(updateLastUsed(patName)));
Mono.error(() -> new DisabledException("Personal access token has been deleted."))
)
.flatMap(pat -> patChecks(pat, jwtId).and(updateLastUsed(patName)).thenReturn(pat))
.map(pat -> {
// Make sure the authorities modifiable
var authorities = new ArrayList<>(jat.getAuthorities());
authorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + ANONYMOUS_ROLE_NAME));
authorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + AUTHENTICATED_ROLE_NAME));
var roles = pat.getSpec().getRoles();
if (roles != null) {
roles.stream()
.map(role -> AuthorityUtils.ROLE_PREFIX + role)
.map(SimpleGrantedAuthority::new)
.forEach(authorities::add);
}
return new JwtAuthenticationToken(jat.getToken(), authorities, jat.getName());
});
}
private Mono<Void> updateLastUsed(String patName) {

View File

@ -138,7 +138,6 @@ public class UserScopedPatHandlerImpl implements UserScopedPatHandler {
.id(tokenId)
.subject(auth.getName())
.issuedAt(clock.instant())
.claim("roles", roles)
.claim("pat_name", createdPat.getMetadata().getName());
var expiresAt = createdPat.getSpec().getExpiresAt();
if (expiresAt != null) {
@ -149,7 +148,6 @@ public class UserScopedPatHandlerImpl implements UserScopedPatHandler {
var jwt = patEncoder.encode(JwtEncoderParameters.from(
headerBuilder.build(),
claimsBuilder.build()));
// TODO Create PAT for the token.
var annotations =
createdPat.getMetadata().getAnnotations();
if (annotations == null) {