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

View File

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