mirror of https://github.com/halo-dev/halo
feat: add personal access token authentication mechanism (#2116)
* feat: add personal access token authentication * fix: merge conflicts * refactor: remove base62 codec * refactor: remove deprecated method * feat: add base64 test * chore: add todo for test only methodspull/2119/head
parent
b5d7f194ef
commit
3c856d04af
|
@ -46,6 +46,7 @@ ext {
|
|||
commonsLang3 = "3.12.0"
|
||||
jsonschemaGenerator = "4.24.3"
|
||||
jsonschemaValidator = "1.0.69"
|
||||
base62 = "0.1.3"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -66,6 +67,7 @@ dependencies {
|
|||
implementation "com.github.victools:jsonschema-module-swagger-2:$jsonschemaGenerator"
|
||||
implementation "com.networknt:json-schema-validator:$jsonschemaValidator"
|
||||
implementation "org.apache.commons:commons-lang3:$commonsLang3"
|
||||
implementation "io.seruco.encoding:base62:$base62"
|
||||
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||
import java.io.IOException;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import javax.crypto.SecretKey;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
|
@ -38,6 +39,9 @@ import org.springframework.security.web.access.intercept.FilterSecurityIntercept
|
|||
import org.springframework.security.web.authentication.logout.LogoutFilter;
|
||||
import org.springframework.security.web.context.SecurityContextHolderFilter;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.identity.apitoken.DefaultPersonalAccessTokenDecoder;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessTokenDecoder;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessTokenUtils;
|
||||
import run.halo.app.identity.authentication.InMemoryOAuth2AuthorizationService;
|
||||
import run.halo.app.identity.authentication.JwtGenerator;
|
||||
import run.halo.app.identity.authentication.OAuth2AuthorizationService;
|
||||
|
@ -48,7 +52,7 @@ import run.halo.app.identity.authentication.ProviderContextFilter;
|
|||
import run.halo.app.identity.authentication.ProviderSettings;
|
||||
import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationFilter;
|
||||
import run.halo.app.identity.authentication.verifier.JwtAccessTokenNonBlockedValidator;
|
||||
import run.halo.app.identity.authentication.verifier.JwtProvidedDecoderAuthenticationManagerResolver;
|
||||
import run.halo.app.identity.authentication.verifier.TokenAuthenticationManagerResolver;
|
||||
import run.halo.app.identity.authorization.DefaultRoleBindingLister;
|
||||
import run.halo.app.identity.authorization.DefaultRoleGetter;
|
||||
import run.halo.app.identity.authorization.RequestInfoAuthorizationManager;
|
||||
|
@ -58,6 +62,7 @@ import run.halo.app.identity.entrypoint.JwtAccessDeniedHandler;
|
|||
import run.halo.app.identity.entrypoint.JwtAuthenticationEntryPoint;
|
||||
import run.halo.app.identity.entrypoint.Oauth2LogoutHandler;
|
||||
import run.halo.app.infra.properties.JwtProperties;
|
||||
import run.halo.app.infra.utils.HaloUtils;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -132,7 +137,8 @@ public class WebSecurityConfig {
|
|||
}
|
||||
|
||||
AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver() {
|
||||
return new JwtProvidedDecoderAuthenticationManagerResolver(jwtDecoder());
|
||||
return new TokenAuthenticationManagerResolver(jwtDecoder(),
|
||||
personalAccessTokenDecoder());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
@ -142,6 +148,13 @@ public class WebSecurityConfig {
|
|||
return authenticationManagerBuilder.getOrBuild();
|
||||
}
|
||||
|
||||
@Bean
|
||||
PersonalAccessTokenDecoder personalAccessTokenDecoder() {
|
||||
String salt = HaloUtils.readClassPathResourceAsString("apiToken.salt");
|
||||
SecretKey secretKey = PersonalAccessTokenUtils.convertStringToSecretKey(salt);
|
||||
return new DefaultPersonalAccessTokenDecoder(oauth2AuthorizationService(), secretKey);
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtDecoder jwtDecoder() {
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(this.key).build();
|
||||
|
@ -199,7 +212,7 @@ public class WebSecurityConfig {
|
|||
// It'll be deleted next time
|
||||
UserDetails user = User.withUsername("user")
|
||||
.password(passwordEncoder().encode("123456"))
|
||||
.roles("USER")
|
||||
.authorities("readPostRole")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
package run.halo.app.identity.apitoken;
|
||||
|
||||
import java.util.Collection;
|
||||
import javax.crypto.SecretKey;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidationException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import run.halo.app.identity.authentication.OAuth2Authorization;
|
||||
import run.halo.app.identity.authentication.OAuth2AuthorizationService;
|
||||
import run.halo.app.identity.authentication.OAuth2TokenType;
|
||||
|
||||
/**
|
||||
* A default implementation of personal access token authentication.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class DefaultPersonalAccessTokenDecoder implements PersonalAccessTokenDecoder {
|
||||
|
||||
private static final String DECODING_ERROR_MESSAGE_TEMPLATE =
|
||||
"An error occurred while attempting to decode the personal access token: %s";
|
||||
|
||||
private OAuth2TokenValidator<PersonalAccessToken> personalAccessTokenValidator =
|
||||
createDefault();
|
||||
|
||||
private final OAuth2AuthorizationService oauth2AuthorizationService;
|
||||
|
||||
private final SecretKey secretKey;
|
||||
|
||||
public DefaultPersonalAccessTokenDecoder(
|
||||
OAuth2AuthorizationService oauth2AuthorizationService, SecretKey secretKey) {
|
||||
this.oauth2AuthorizationService = oauth2AuthorizationService;
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link PersonalAccessToken} Validator.
|
||||
*
|
||||
* @param personalAccessTokenValidator - the PersonalAccessToken Validator to use
|
||||
*/
|
||||
public void setTokenValidator(
|
||||
OAuth2TokenValidator<PersonalAccessToken> personalAccessTokenValidator) {
|
||||
Assert.notNull(personalAccessTokenValidator, "personalAccessTokenValidator cannot be null");
|
||||
this.personalAccessTokenValidator = personalAccessTokenValidator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersonalAccessToken decode(String token) throws PersonalAccessTokenException {
|
||||
preValidate(token);
|
||||
PersonalAccessToken personalAccessToken = createPersonalAccessToken(token);
|
||||
return validate(personalAccessToken);
|
||||
}
|
||||
|
||||
private void preValidate(String token) {
|
||||
if (secretKey == null) {
|
||||
return;
|
||||
}
|
||||
boolean matches = PersonalAccessTokenUtils.verifyChecksum(token, secretKey);
|
||||
if (matches) {
|
||||
return;
|
||||
}
|
||||
throw new PersonalAccessTokenException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to verify the personal access token"));
|
||||
}
|
||||
|
||||
private PersonalAccessToken createPersonalAccessToken(String token) {
|
||||
OAuth2Authorization oauth2Authorization = oauth2AuthorizationService.findByToken(token,
|
||||
OAuth2TokenType.ACCESS_TOKEN);
|
||||
if (oauth2Authorization == null) {
|
||||
throw new PersonalAccessTokenException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to retrieve personal access token"));
|
||||
}
|
||||
OAuth2Authorization.Token<OAuth2AccessToken> accessTokenToken =
|
||||
oauth2Authorization.getToken(OAuth2AccessToken.class);
|
||||
if (accessTokenToken == null) {
|
||||
throw new PersonalAccessTokenException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE,
|
||||
"Failed to retrieve personal access token"));
|
||||
}
|
||||
return createPersonalAccessToken(accessTokenToken.getToken());
|
||||
}
|
||||
|
||||
private PersonalAccessToken createPersonalAccessToken(OAuth2AccessToken token) {
|
||||
return new PersonalAccessToken(token.getTokenValue(), token.getIssuedAt(),
|
||||
token.getExpiresAt(), token.getScopes());
|
||||
}
|
||||
|
||||
private PersonalAccessToken validate(PersonalAccessToken token) {
|
||||
OAuth2TokenValidatorResult result = this.personalAccessTokenValidator.validate(token);
|
||||
if (result.hasErrors()) {
|
||||
Collection<OAuth2Error> errors = result.getErrors();
|
||||
String validationErrorString = getValidationExceptionMessage(errors);
|
||||
throw new JwtValidationException(validationErrorString, errors);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
private String getValidationExceptionMessage(Collection<OAuth2Error> errors) {
|
||||
for (OAuth2Error oauth2Error : errors) {
|
||||
if (!StringUtils.hasText(oauth2Error.getDescription())) {
|
||||
return String.format(DECODING_ERROR_MESSAGE_TEMPLATE, oauth2Error.getDescription());
|
||||
}
|
||||
}
|
||||
return "Unable to validate personal access token";
|
||||
}
|
||||
|
||||
public static OAuth2TokenValidator<PersonalAccessToken> createDefault() {
|
||||
return new DelegatingOAuth2TokenValidator<>(new PersonalAccessTokenTimestampValidator());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package run.halo.app.identity.apitoken;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
|
||||
/**
|
||||
* <p>An implementation of an {@link AbstractOAuth2Token} representing a personal access token.</p>
|
||||
* <p>A personal-access-token is a credential that represents an authorization granted by the
|
||||
* resource owner.</p>
|
||||
* <p>It is primarily used by the client to access protected resources on either a resource
|
||||
* server.</p>
|
||||
* <p>All personal access tokens created by administrators of the {@code Halo} application are
|
||||
* permanent tokens that cannot be regenerated.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class PersonalAccessToken extends OAuth2AccessToken {
|
||||
|
||||
public static final AuthorizationGrantType PERSONAL_ACCESS_TOKEN =
|
||||
new AuthorizationGrantType("personal_access_token");
|
||||
|
||||
/**
|
||||
* Constructs an {@code PersonalAccessToken} using the provided parameters.
|
||||
*
|
||||
* @param tokenValue the token value
|
||||
* @param issuedAt the time at which the token was issued
|
||||
*/
|
||||
public PersonalAccessToken(String tokenValue, Instant issuedAt) {
|
||||
this(tokenValue, issuedAt, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code PersonalAccessToken} using the provided parameters.
|
||||
*
|
||||
* @param tokenValue the token value
|
||||
* @param issuedAt the time at which the token was issued
|
||||
* @param expiresAt the time at which the token expires
|
||||
*/
|
||||
public PersonalAccessToken(String tokenValue, Instant issuedAt, Instant expiresAt) {
|
||||
this(tokenValue, issuedAt, expiresAt, Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code PersonalAccessToken} using the provided parameters.
|
||||
*
|
||||
* @param tokenValue the token value
|
||||
* @param issuedAt the time at which the token was issued
|
||||
* @param expiresAt the time at which the token expires
|
||||
* @param roles role names
|
||||
*/
|
||||
public PersonalAccessToken(String tokenValue, Instant issuedAt, Instant expiresAt,
|
||||
Set<String> roles) {
|
||||
super(TokenType.BEARER, tokenValue, issuedAt, expiresAt, roles);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package run.halo.app.identity.apitoken;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.Transient;
|
||||
import run.halo.app.identity.authentication.verifier.AbstractOAuth2TokenAuthenticationToken;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Transient
|
||||
public class PersonalAccessTokenAuthenticationToken extends
|
||||
AbstractOAuth2TokenAuthenticationToken<PersonalAccessToken> {
|
||||
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Constructs a {@code PersonalAccessToken} using the provided parameters.
|
||||
*
|
||||
* @param personalAccessToken the PersonalAccessToken
|
||||
*/
|
||||
public PersonalAccessTokenAuthenticationToken(PersonalAccessToken personalAccessToken) {
|
||||
super(personalAccessToken);
|
||||
this.name = personalAccessToken.getTokenValue();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a {@code PersonalAccessTokenAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param personalAccessToken the PersonalAccessToken
|
||||
* @param authorities the authorities assigned to the PersonalAccessToken
|
||||
*/
|
||||
public PersonalAccessTokenAuthenticationToken(PersonalAccessToken personalAccessToken,
|
||||
Collection<? extends GrantedAuthority> authorities) {
|
||||
super(personalAccessToken, authorities);
|
||||
this.setAuthenticated(true);
|
||||
this.name = personalAccessToken.getTokenValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code PersonalAccessTokenAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param personalAccessToken the PersonalAccessToken
|
||||
* @param authorities the authorities assigned to the PersonalAccessToken
|
||||
* @param name the principal name
|
||||
*/
|
||||
public PersonalAccessTokenAuthenticationToken(PersonalAccessToken personalAccessToken,
|
||||
Collection<? extends GrantedAuthority> authorities,
|
||||
String name) {
|
||||
super(personalAccessToken, authorities);
|
||||
this.setAuthenticated(true);
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getTokenAttributes() {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* The principal name which is, by default, the {@link PersonalAccessToken}'s tokenValue.
|
||||
*/
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package run.halo.app.identity.apitoken;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface PersonalAccessTokenDecoder {
|
||||
|
||||
PersonalAccessToken decode(String token) throws PersonalAccessTokenException;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package run.halo.app.identity.apitoken;
|
||||
|
||||
/**
|
||||
* Base exception for all personal access token related errors.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class PersonalAccessTokenException extends RuntimeException {
|
||||
|
||||
public PersonalAccessTokenException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public PersonalAccessTokenException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package run.halo.app.identity.apitoken;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.crypto.SecretKey;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationToken;
|
||||
import run.halo.app.identity.authentication.verifier.InvalidBearerTokenException;
|
||||
|
||||
/**
|
||||
* <p>An AuthenticationProvider implementation of the personal-access-token based
|
||||
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>
|
||||
* s for protecting server resources.</p>
|
||||
* <p>This {@link AuthenticationProvider} is responsible for decoding and verifying
|
||||
* {@link PersonalAccessTokenUtils#generate(PersonalAccessTokenType, SecretKey)}-generated access
|
||||
* token.</p>
|
||||
*
|
||||
* <p>The composition format of personal-access-token is:
|
||||
* <pre>{two letter type prefix}_{32-bit secure random value}{checksum}</pre>
|
||||
* Token type prefix is an explicit way to make a token recognizable. such as {@code h}
|
||||
* represents {@code halo} and {@code c} represents the content api.</p>
|
||||
* <p>Make these prefixes legible in the token to improve readability. Therefore, a separator is
|
||||
* added:{@code _} and when you double-click it, it reliably selects the entire token.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class PersonalAccessTokenProvider implements AuthenticationProvider {
|
||||
private final PersonalAccessTokenDecoder personalAccessTokenDecoder;
|
||||
|
||||
public PersonalAccessTokenProvider(PersonalAccessTokenDecoder personalAccessTokenDecoder) {
|
||||
this.personalAccessTokenDecoder = personalAccessTokenDecoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication)
|
||||
throws AuthenticationException {
|
||||
if (!(authentication instanceof BearerTokenAuthenticationToken bearer)) {
|
||||
return null;
|
||||
}
|
||||
PersonalAccessToken accessToken = getPersonalAccessToken(bearer.getToken());
|
||||
|
||||
PersonalAccessTokenAuthenticationToken token = convert(accessToken);
|
||||
token.setDetails(bearer.getDetails());
|
||||
log.debug("Authenticated token");
|
||||
return token;
|
||||
}
|
||||
|
||||
private PersonalAccessTokenAuthenticationToken convert(PersonalAccessToken accessToken) {
|
||||
Collection<GrantedAuthority> authorities = accessToken.getScopes()
|
||||
.stream()
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
return new PersonalAccessTokenAuthenticationToken(accessToken, authorities);
|
||||
}
|
||||
|
||||
private PersonalAccessToken getPersonalAccessToken(String tokenValue) {
|
||||
try {
|
||||
return this.personalAccessTokenDecoder.decode(tokenValue);
|
||||
} catch (PersonalAccessTokenException failed) {
|
||||
log.debug("Failed to authenticate since the personal-access-token was invalid");
|
||||
throw new InvalidBearerTokenException(failed.getMessage(), failed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return PersonalAccessToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package run.halo.app.identity.apitoken;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* <p>An implementation of {@link OAuth2TokenValidator} for verifying personal access token.</p>
|
||||
* <p>Because clocks can differ between the personal-access-token source, say the Authorization
|
||||
* Server, and its destination, say the Resource Server, there is a default clock leeway
|
||||
* exercised when deciding if the current time is within the {@link PersonalAccessToken}'s
|
||||
* specified operating window.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @see OAuth2TokenValidator
|
||||
* @see PersonalAccessToken
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class PersonalAccessTokenTimestampValidator implements
|
||||
OAuth2TokenValidator<PersonalAccessToken> {
|
||||
|
||||
private static final Duration DEFAULT_MAX_CLOCK_SKEW = Duration.of(60, ChronoUnit.SECONDS);
|
||||
|
||||
private final Duration clockSkew;
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
/**
|
||||
* A basic instance with no custom verification and the default max clock skew.
|
||||
*/
|
||||
public PersonalAccessTokenTimestampValidator() {
|
||||
this(DEFAULT_MAX_CLOCK_SKEW);
|
||||
}
|
||||
|
||||
public PersonalAccessTokenTimestampValidator(Duration clockSkew) {
|
||||
Assert.notNull(clockSkew, "clockSkew cannot be null");
|
||||
this.clockSkew = clockSkew;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidatorResult validate(PersonalAccessToken token) {
|
||||
Assert.notNull(token, "personalAccessToken cannot be null");
|
||||
Instant expiry = token.getExpiresAt();
|
||||
if (expiry != null) {
|
||||
if (Instant.now(this.clock).minus(this.clockSkew).isAfter(expiry)) {
|
||||
OAuth2Error oauth2Error =
|
||||
createOauth2Error(
|
||||
String.format("personal-access-token expired at %s", token.getExpiresAt()));
|
||||
return OAuth2TokenValidatorResult.failure(oauth2Error);
|
||||
}
|
||||
}
|
||||
Instant notBefore = token.getIssuedAt();
|
||||
if (notBefore != null) {
|
||||
if (Instant.now(this.clock).plus(this.clockSkew).isBefore(notBefore)) {
|
||||
OAuth2Error oauth2Error = createOauth2Error(
|
||||
String.format("personal-access-token used before %s", token.getIssuedAt()));
|
||||
return OAuth2TokenValidatorResult.failure(oauth2Error);
|
||||
}
|
||||
}
|
||||
return OAuth2TokenValidatorResult.success();
|
||||
}
|
||||
|
||||
private OAuth2Error createOauth2Error(String reason) {
|
||||
log.debug(reason);
|
||||
return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, reason,
|
||||
"https://github.com/halo-dev/rfcs/blob/main/identity/003-encryption.md");
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this {@link Clock} with {@link Instant#now()} for assessing timestamp validity.
|
||||
*
|
||||
* @param clock A clock providing access to the current instant
|
||||
*/
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "clock cannot be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package run.halo.app.identity.apitoken;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* <p>Personal access token type.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public record PersonalAccessTokenType(String value) {
|
||||
|
||||
public static final PersonalAccessTokenType ADMIN_TOKEN = new PersonalAccessTokenType("ha");
|
||||
public static final PersonalAccessTokenType CONTENT_TOKEN = new PersonalAccessTokenType("hc");
|
||||
|
||||
/**
|
||||
* Constructs an {@code PersonalAccessTokenType} using the provided value.
|
||||
*
|
||||
* @param value the value of the token type
|
||||
*/
|
||||
public PersonalAccessTokenType {
|
||||
Assert.hasText(value, "value cannot be empty");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package run.halo.app.identity.apitoken;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.zip.CRC32;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.security.crypto.codec.Hex;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
import run.halo.app.infra.utils.Base62Utils;
|
||||
|
||||
/**
|
||||
* Tool class for generating and verifying personal access token.
|
||||
*
|
||||
* @author guqing
|
||||
* @see
|
||||
* <a href="https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/">githubs-new-authentication-token-formats</a>
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class PersonalAccessTokenUtils {
|
||||
|
||||
/**
|
||||
* <p>Generate personal access token through secretKey.</p>
|
||||
* <p>The format is tokenValue + 8-bit checksum.</p>
|
||||
*
|
||||
* @param secretKey The secretKey is used to generate a salt
|
||||
* @return personal access token
|
||||
*/
|
||||
public static String generate(PersonalAccessTokenType tokenType, SecretKey secretKey) {
|
||||
// Generate 32-bit random API token.
|
||||
String apiToken = new String(Hex.encode(KeyGenerators.secureRandom(16).generateKey()));
|
||||
// crc32(apiToken + salt)
|
||||
String salt = convertSecretKeyToString(secretKey);
|
||||
String checksum = crc32((apiToken + salt).getBytes());
|
||||
// Encode it as base62
|
||||
String encodedValue = Base62Utils.encode(apiToken + checksum);
|
||||
return String.format("%s_%s", tokenType.value(), encodedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Decoded the personalAccessToken through base62, the intercepted 8-bit checksum is
|
||||
* compared with the result generated by the checksum rule.</p>
|
||||
* <p>If it matches, it returns {@code true}, otherwise it returns {@code false}.</p>
|
||||
*
|
||||
* @param personalAccessToken personal access token to verify
|
||||
* @param secretKey The secretKey is used to generate a salt
|
||||
* @return {@code true} if the original checksum matches the generated checksum,otherwise
|
||||
* it returns {@code false}
|
||||
*/
|
||||
public static boolean verifyChecksum(String personalAccessToken, SecretKey secretKey) {
|
||||
String tokenValue = PersonalTokenTypeUtils.removeTypePrefix(personalAccessToken);
|
||||
String decodedToken = Base62Utils.decodeToString(tokenValue);
|
||||
|
||||
int length = decodedToken.length();
|
||||
// Gets api token and checksum from decodedToken.
|
||||
String apiToken = decodedToken.substring(0, length - 8);
|
||||
String originalChecksum = decodedToken.substring(length - 8);
|
||||
|
||||
String salt = convertSecretKeyToString(secretKey);
|
||||
String checksum = crc32((apiToken + salt).getBytes());
|
||||
return StringUtils.equals(originalChecksum, checksum);
|
||||
}
|
||||
|
||||
public static String convertSecretKeyToString(SecretKey secretKey) {
|
||||
byte[] rawData = secretKey.getEncoded();
|
||||
return Base64.getEncoder().encodeToString(rawData);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Generate 256 bit {@link SecretKey} through AES algorithm.</p>
|
||||
*
|
||||
* @return secret key
|
||||
*/
|
||||
public static SecretKey generateSecretKey() {
|
||||
try {
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
|
||||
keyGenerator.init(256);
|
||||
return keyGenerator.generateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Convert the encoded value of 256 bit string generated by AES algorithm to
|
||||
* {@link SecretKey}.</p>
|
||||
*
|
||||
* @return secret key
|
||||
*/
|
||||
public static SecretKey convertStringToSecretKey(String encodedKey) {
|
||||
byte[] decodedKey = Base64.getDecoder().decode(encodedKey);
|
||||
return new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");
|
||||
}
|
||||
|
||||
private static String crc32(byte[] array) {
|
||||
CRC32 crc32 = new CRC32();
|
||||
crc32.update(array);
|
||||
return Long.toHexString(crc32.getValue());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package run.halo.app.identity.apitoken;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An util for Personal access token type.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class PersonalTokenTypeUtils {
|
||||
private static final String TOKEN_TYPE_SEPARATOR = "_";
|
||||
|
||||
/**
|
||||
* Remove the type prefix in the personal access token string.
|
||||
*
|
||||
* @param tokenValue personal access token
|
||||
* @return token removed prefix
|
||||
*/
|
||||
public static String removeTypePrefix(String tokenValue) {
|
||||
String adminType = PersonalAccessTokenType.ADMIN_TOKEN.value() + TOKEN_TYPE_SEPARATOR;
|
||||
if (StringUtils.startsWith(tokenValue, adminType)) {
|
||||
return StringUtils.substringAfter(tokenValue, adminType);
|
||||
}
|
||||
String contentType = PersonalAccessTokenType.CONTENT_TOKEN.value() + TOKEN_TYPE_SEPARATOR;
|
||||
if (StringUtils.startsWith(tokenValue, contentType)) {
|
||||
return StringUtils.substringAfter(tokenValue, contentType);
|
||||
}
|
||||
return tokenValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Judge whether it is a personal access token of admin type.
|
||||
*
|
||||
* @param tokenValue personal access token
|
||||
* @return {@code true} if it is a token of admin type, otherwise {@code false}
|
||||
*/
|
||||
public static boolean isAdminToken(String tokenValue) {
|
||||
Assert.notNull(tokenValue, "The tokenValue must not be null.");
|
||||
String adminType = PersonalAccessTokenType.ADMIN_TOKEN.value() + TOKEN_TYPE_SEPARATOR;
|
||||
return StringUtils.startsWith(tokenValue, adminType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Judge whether it is a personal access token of content type.
|
||||
*
|
||||
* @param tokenValue personal access token
|
||||
* @return {@code true} if it is a token of content type, otherwise {@code false}
|
||||
*/
|
||||
public static boolean isContentToken(String tokenValue) {
|
||||
String contentType = PersonalAccessTokenType.CONTENT_TOKEN.value() + TOKEN_TYPE_SEPARATOR;
|
||||
return StringUtils.startsWith(tokenValue, contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether there is a personal access token type prefix.
|
||||
*
|
||||
* @param tokenValue personal access token
|
||||
* @return {@code true} if it is starts with {@link PersonalAccessTokenType#ADMIN_TOKEN} or
|
||||
* {@link PersonalAccessTokenType#CONTENT_TOKEN} prefix, otherwise {@code false}
|
||||
*/
|
||||
public static boolean isPersonalAccessToken(String tokenValue) {
|
||||
return isAdminToken(tokenValue) || isContentToken(tokenValue);
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package run.halo.app.identity.authentication.verifier;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationManagerResolver;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A jwt resolver for {@link AuthenticationManager} use {@link JwtDecoder}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public record JwtProvidedDecoderAuthenticationManagerResolver(JwtDecoder jwtDecoder)
|
||||
implements AuthenticationManagerResolver<HttpServletRequest> {
|
||||
public JwtProvidedDecoderAuthenticationManagerResolver {
|
||||
Assert.notNull(jwtDecoder, "jwtDecoder cannot be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationManager resolve(HttpServletRequest request) {
|
||||
return new JwtAuthenticationProvider(jwtDecoder)::authenticate;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package run.halo.app.identity.authentication.verifier;
|
||||
|
||||
import com.nimbusds.jwt.JWTParser;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.text.ParseException;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationManagerResolver;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessTokenDecoder;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessTokenProvider;
|
||||
import run.halo.app.identity.apitoken.PersonalTokenTypeUtils;
|
||||
|
||||
/**
|
||||
* A token resolver for {@link AuthenticationManager} use {@link JwtDecoder} and
|
||||
* {@link PersonalAccessTokenDecoder}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public record TokenAuthenticationManagerResolver(JwtDecoder jwtDecoder,
|
||||
PersonalAccessTokenDecoder personalTokenDecoder)
|
||||
implements AuthenticationManagerResolver<HttpServletRequest> {
|
||||
|
||||
private static final DefaultBearerTokenResolver defaultBearerTokenResolver =
|
||||
new DefaultBearerTokenResolver();
|
||||
|
||||
public TokenAuthenticationManagerResolver {
|
||||
Assert.notNull(jwtDecoder, "jwtDecoder cannot be null");
|
||||
Assert.notNull(personalTokenDecoder, "personalAccessTokenDecoder cannot be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationManager resolve(HttpServletRequest request) {
|
||||
|
||||
String bearerToken = defaultBearerTokenResolver.resolve(request);
|
||||
|
||||
if (useJwt(bearerToken)) {
|
||||
return new JwtAuthenticationProvider(jwtDecoder)::authenticate;
|
||||
} else if (PersonalTokenTypeUtils.isPersonalAccessToken(bearerToken)) {
|
||||
return new PersonalAccessTokenProvider(personalTokenDecoder)::authenticate;
|
||||
}
|
||||
|
||||
return authentication -> {
|
||||
throw new AuthenticationServiceException("Cannot authenticate " + authentication);
|
||||
};
|
||||
}
|
||||
|
||||
private boolean useJwt(String token) {
|
||||
try {
|
||||
JWTParser.parse(token);
|
||||
return true;
|
||||
} catch (ParseException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -76,7 +76,7 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
|||
|
||||
String roleBindingDescriber(RoleBinding roleBinding, Subject subject) {
|
||||
String describeSubject = String.format("%s %s", subject.kind, subject.name);
|
||||
return String.format("RoleBinding %s of %s %s to %s", roleBinding.metadata().getName(),
|
||||
return String.format("RoleBinding %s of %s %s to %s", roleBinding.getMetadata().getName(),
|
||||
roleBinding.roleRef.getKind(), roleBinding.roleRef.getName(), describeSubject);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,25 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.crypto.SecretKey;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.Schemes;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessToken;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessTokenType;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessTokenUtils;
|
||||
import run.halo.app.identity.authentication.OAuth2Authorization;
|
||||
import run.halo.app.identity.authentication.OAuth2AuthorizationService;
|
||||
import run.halo.app.identity.authorization.PolicyRule;
|
||||
import run.halo.app.identity.authorization.Role;
|
||||
import run.halo.app.infra.utils.HaloUtils;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -12,8 +27,68 @@ import run.halo.app.identity.authorization.Role;
|
|||
*/
|
||||
@Component
|
||||
public class SchemeInitializer implements ApplicationListener<ApplicationStartedEvent> {
|
||||
|
||||
private final OAuth2AuthorizationService oauth2AuthorizationService;
|
||||
|
||||
private final ExtensionClient extensionClient;
|
||||
|
||||
public SchemeInitializer(OAuth2AuthorizationService oauth2AuthorizationService,
|
||||
ExtensionClient extensionClient) {
|
||||
this.oauth2AuthorizationService = oauth2AuthorizationService;
|
||||
this.extensionClient = extensionClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationStartedEvent event) {
|
||||
Schemes.INSTANCE.register(Role.class);
|
||||
|
||||
// TODO These test only methods will be removed in the future
|
||||
initRoleForTesting();
|
||||
initPersonalAccessTokenForTesting();
|
||||
}
|
||||
|
||||
|
||||
private void initRoleForTesting() {
|
||||
Role role = new Role();
|
||||
role.setApiVersion("v1alpha1");
|
||||
role.setKind("Role");
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("readPostRole");
|
||||
role.setMetadata(metadata);
|
||||
List<PolicyRule> rules = List.of(
|
||||
new PolicyRule.Builder().apiGroups("").resources("posts").verbs("list", "get")
|
||||
.build(),
|
||||
new PolicyRule.Builder().apiGroups("").resources("categories").verbs("*")
|
||||
.build(),
|
||||
new PolicyRule.Builder().nonResourceURLs("/healthy").verbs("get", "post", "head")
|
||||
.build()
|
||||
);
|
||||
role.setRules(rules);
|
||||
|
||||
extensionClient.create(role);
|
||||
}
|
||||
|
||||
private void initPersonalAccessTokenForTesting() {
|
||||
String salt = HaloUtils.readClassPathResourceAsString("apiToken.salt");
|
||||
SecretKey secretKey = PersonalAccessTokenUtils.convertStringToSecretKey(salt);
|
||||
String tokenValue =
|
||||
PersonalAccessTokenUtils.generate(PersonalAccessTokenType.ADMIN_TOKEN, secretKey);
|
||||
|
||||
Set<String> roles = Set.of("readPostRole");
|
||||
OAuth2AccessToken personalAccessToken =
|
||||
new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, tokenValue, Instant.now(),
|
||||
Instant.now().plus(2, ChronoUnit.HOURS), roles);
|
||||
|
||||
System.out.println(
|
||||
"Initializing a personal access token is only for development or testing: "
|
||||
+ tokenValue);
|
||||
|
||||
OAuth2Authorization authorization = new OAuth2Authorization.Builder()
|
||||
.id(HaloUtils.simpleUUID())
|
||||
.token(personalAccessToken)
|
||||
.principalName(tokenValue)
|
||||
.authorizationGrantType(PersonalAccessToken.PERSONAL_ACCESS_TOKEN)
|
||||
.build();
|
||||
oauth2AuthorizationService.save(authorization);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import io.seruco.encoding.base62.Base62;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* <p>Base62 tool class, which provides the encoding and decoding scheme of base62.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class Base62Utils {
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
private static final Base62 INSTANCE = Base62.createInstance();
|
||||
|
||||
public static String encode(String source) {
|
||||
return encode(source, DEFAULT_CHARSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base62 encode.
|
||||
*
|
||||
* @param source the encoded base62 string
|
||||
* @param charset the charset default is utf_8
|
||||
* @return encoded string by base62
|
||||
*/
|
||||
public static String encode(String source, Charset charset) {
|
||||
return encode(StringUtils.getBytes(source, charset));
|
||||
}
|
||||
|
||||
public static String encode(byte[] source) {
|
||||
return new String(INSTANCE.encode(source));
|
||||
}
|
||||
|
||||
/**
|
||||
* Base62 decode.
|
||||
*
|
||||
* @param base62Str the Base62 decoded string
|
||||
* @return decoded bytes
|
||||
*/
|
||||
public static byte[] decode(String base62Str) {
|
||||
return decode(StringUtils.getBytes(base62Str, DEFAULT_CHARSET));
|
||||
}
|
||||
|
||||
public static byte[] decode(byte[] base62bytes) {
|
||||
return INSTANCE.decode(base62bytes);
|
||||
}
|
||||
|
||||
public static String decodeToString(String source) {
|
||||
return decodeToString(source, DEFAULT_CHARSET);
|
||||
}
|
||||
|
||||
public static String decodeToString(String source, Charset charset) {
|
||||
return StringUtils.toEncodedString(decode(source), charset);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,12 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -16,4 +21,21 @@ public class HaloUtils {
|
|||
public static String simpleUUID() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Read the file under the classpath as a string.</p>
|
||||
*
|
||||
* @param location the file location relative to classpath
|
||||
* @return file content
|
||||
*/
|
||||
public static String readClassPathResourceAsString(String location) {
|
||||
ClassPathResource classPathResource = new ClassPathResource(location);
|
||||
try (InputStream inputStream = classPathResource.getInputStream()) {
|
||||
return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Failed to read class path file as string from location [%s]",
|
||||
location), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
bySwF9ZxJ2JpQYs830+eA3Fw6O7F/ULDvyYGEPaZKwY=
|
|
@ -0,0 +1,47 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.seruco.encoding.base62.Base62;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link Base62}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class Base62UtilsTest {
|
||||
|
||||
@Test
|
||||
void encode() {
|
||||
getNaiveTestSet().forEach(
|
||||
(str, encoded) -> assertThat(Base62Utils.encode(str)).isEqualTo(encoded));
|
||||
}
|
||||
|
||||
@Test
|
||||
void decodeToString() {
|
||||
getNaiveTestSet().forEach(
|
||||
(str, encoded) -> assertThat(Base62Utils.decodeToString(encoded)).isEqualTo(str));
|
||||
}
|
||||
|
||||
public static Map<String, String> getNaiveTestSet() {
|
||||
Map<String, String> testSet = new HashMap<>();
|
||||
|
||||
testSet.put("", "");
|
||||
testSet.put("a", "1Z");
|
||||
testSet.put("Hello", "5TP3P3v");
|
||||
testSet.put("Hello world!", "T8dgcjRGuYUueWht");
|
||||
testSet.put("Just a test", "7G0iTmJjQFG2t6K");
|
||||
testSet.put("!!!!!!!!!!!!!!!!!", "4A7f43EVXQoS6Am897ZKbAn");
|
||||
testSet.put("0123456789", "18XU2xYejWO9d3");
|
||||
testSet.put("The quick brown fox jumps over the lazy dog",
|
||||
"83UM8dOjD4xrzASgmqLOXTgTagvV1jPegUJ39mcYnwHwTlzpdfKXvpp4RL");
|
||||
testSet.put("Sphinx of black quartz, judge my vow",
|
||||
"1Ul5yQGNM8YFBp3sz19dYj1kTp95OW7jI8pTcTP5JhYjIaFmx");
|
||||
|
||||
return testSet;
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import java.io.IOException;
|
|||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.List;
|
||||
import javax.crypto.SecretKey;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -41,6 +42,9 @@ import org.springframework.security.web.authentication.logout.LogoutFilter;
|
|||
import org.springframework.security.web.context.SecurityContextHolderFilter;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.identity.apitoken.DefaultPersonalAccessTokenDecoder;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessTokenDecoder;
|
||||
import run.halo.app.identity.apitoken.PersonalAccessTokenUtils;
|
||||
import run.halo.app.identity.authentication.InMemoryOAuth2AuthorizationService;
|
||||
import run.halo.app.identity.authentication.JwtGenerator;
|
||||
import run.halo.app.identity.authentication.OAuth2AuthorizationService;
|
||||
|
@ -51,7 +55,7 @@ import run.halo.app.identity.authentication.ProviderContextFilter;
|
|||
import run.halo.app.identity.authentication.ProviderSettings;
|
||||
import run.halo.app.identity.authentication.verifier.BearerTokenAuthenticationFilter;
|
||||
import run.halo.app.identity.authentication.verifier.JwtAccessTokenNonBlockedValidator;
|
||||
import run.halo.app.identity.authentication.verifier.JwtProvidedDecoderAuthenticationManagerResolver;
|
||||
import run.halo.app.identity.authentication.verifier.TokenAuthenticationManagerResolver;
|
||||
import run.halo.app.identity.authorization.PolicyRule;
|
||||
import run.halo.app.identity.authorization.RequestInfoAuthorizationManager;
|
||||
import run.halo.app.identity.authorization.Role;
|
||||
|
@ -60,6 +64,7 @@ import run.halo.app.identity.authorization.RoleRef;
|
|||
import run.halo.app.identity.authorization.Subject;
|
||||
import run.halo.app.identity.entrypoint.Oauth2LogoutHandler;
|
||||
import run.halo.app.infra.properties.JwtProperties;
|
||||
import run.halo.app.infra.utils.HaloUtils;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -160,11 +165,18 @@ public class TestWebSecurityConfig {
|
|||
}
|
||||
|
||||
AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver() {
|
||||
return new JwtProvidedDecoderAuthenticationManagerResolver(jwtDecoder());
|
||||
return new TokenAuthenticationManagerResolver(jwtDecoder(), personalAccessTokenDecoder());
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthenticationManager authenticationManager() throws Exception {
|
||||
PersonalAccessTokenDecoder personalAccessTokenDecoder() {
|
||||
String salt = HaloUtils.readClassPathResourceAsString("apiToken.salt");
|
||||
SecretKey secretKey = PersonalAccessTokenUtils.convertStringToSecretKey(salt);
|
||||
return new DefaultPersonalAccessTokenDecoder(oauth2AuthorizationService(), secretKey);
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthenticationManager authenticationManager() {
|
||||
authenticationManagerBuilder.authenticationProvider(passwordAuthenticationProvider())
|
||||
.authenticationProvider(oauth2RefreshTokenAuthenticationProvider());
|
||||
return authenticationManagerBuilder.getOrBuild();
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
bySwF9ZxJ2JpQYs830+eA3Fw6O7F/ULDvyYGEPaZKwY=
|
Loading…
Reference in New Issue