Replace webmvc with webflux (#2138)

* Replace webmvc to webflux

Signed-off-by: johnniang <johnniang@fastmail.com>

* Remove jetty dependency

Signed-off-by: johnniang <johnniang@fastmail.com>

* Refactor authentication module

* Refactor authentication module

* Migrate authorization module

* Refactor Login components

* Fix broken imports

* Upgrade springdoc version

* Refine security matcher using pathMatchers utility
pull/2132/head
John Niang 2022-06-07 10:20:12 +08:00 committed by GitHub
parent 19db04c430
commit 1024f71635
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
135 changed files with 1032 additions and 8010 deletions

View File

@ -18,10 +18,13 @@ repositories {
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'https://maven.aliyun.com/repository/spring/' }
maven { url 'https://repo.spring.io/milestone' }
mavenLocal()
mavenCentral()
}
configurations {
implementation {
exclude module: "spring-boot-starter-tomcat"
@ -54,13 +57,16 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-oauth2-jose'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "org.springframework.boot:spring-boot-starter-jetty"
implementation 'org.springframework.security:spring-security-oauth2-resource-server'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.0-M2'
implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.0.0-M3'
implementation "org.flywaydb:flyway-core"
implementation "org.flywaydb:flyway-mysql"
@ -84,6 +90,7 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'io.projectreactor:reactor-test'
}
tasks.named('test') {

4
docs/security.puml Normal file
View File

@ -0,0 +1,4 @@
@startuml
ExceptionHandlingWebHandler -> FilteringWebHandler
FilteringWebHandler contains filters and DispatcherHandler
@enduml

View File

@ -2,6 +2,9 @@ package run.halo.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.infra.properties.JwtProperties;
/**
* Halo main class.
@ -12,6 +15,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* @date 2017-11-14
*/
@SpringBootApplication
@EnableConfigurationProperties({HaloProperties.class, JwtProperties.class})
public class Application {
public static void main(String[] args) {

View File

@ -0,0 +1,26 @@
package run.halo.app.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Bean
OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("basicScheme", new SecurityScheme()
.type(SecurityScheme.Type.HTTP).scheme("basic"))
.addSecuritySchemes("bearerAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT"))
)
.info(new Info().title("Halo Next API")
.version("2.0.0"));
}
}

View File

@ -0,0 +1,36 @@
package run.halo.app.config;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
import org.springframework.web.reactive.result.view.ViewResolver;
@Configuration
@EnableWebFlux
public class WebFluxConfig implements WebFluxConfigurer {
@Bean
ServerResponse.Context context(CodecConfigurer codec,
ViewResolutionResultHandler resultHandler) {
return new ServerResponse.Context() {
@Override
@NonNull
public List<HttpMessageWriter<?>> messageWriters() {
return codec.getWriters();
}
@Override
@NonNull
public List<ViewResolver> viewResolvers() {
return resultHandler.getViewResolvers();
}
};
}
}

View File

@ -1,27 +0,0 @@
package run.halo.app.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import run.halo.app.Application;
/**
* Spring web mvc config.
*
* @author guqing
* @date 2022-04-12
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/api/v1",
c -> HandlerTypePredicate.forAnnotation(RestController.class)
.and(HandlerTypePredicate.forBasePackage(Application.class.getPackageName()))
.test(c)
);
}
}

View File

@ -1,221 +0,0 @@
package run.halo.app.config;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
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;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
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;
import run.halo.app.identity.authentication.OAuth2PasswordAuthenticationProvider;
import run.halo.app.identity.authentication.OAuth2RefreshTokenAuthenticationProvider;
import run.halo.app.identity.authentication.OAuth2TokenEndpointFilter;
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.TokenAuthenticationManagerResolver;
import run.halo.app.identity.authorization.DefaultRoleGetter;
import run.halo.app.identity.authorization.RequestInfoAuthorizationManager;
import run.halo.app.identity.authorization.RoleGetter;
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
* @since 2022-04-12
*/
@EnableWebSecurity
@EnableConfigurationProperties(JwtProperties.class)
public class WebSecurityConfig {
private final RSAPublicKey key;
private final RSAPrivateKey priv;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final ExtensionClient extensionClient;
public WebSecurityConfig(JwtProperties jwtProperties,
AuthenticationManagerBuilder authenticationManagerBuilder,
ExtensionClient extensionClient) throws IOException {
this.key = jwtProperties.readPublicKey();
this.priv = jwtProperties.readPrivateKey();
this.authenticationManagerBuilder = authenticationManagerBuilder;
this.extensionClient = extensionClient;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
ProviderSettings providerSettings = providerSettings();
ProviderContextFilter providerContextFilter = new ProviderContextFilter(providerSettings);
http
.authorizeHttpRequests((authorize) -> authorize
.antMatchers(providerSettings.getTokenEndpoint()).permitAll()
// for static path
.antMatchers("/static/js/**").permitAll()
// for swagger ui
.antMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.antMatchers("/logout").authenticated()
.antMatchers("/api/**", "/apis/**").access(requestInfoAuthorizationManager())
.anyRequest().access(requestInfoAuthorizationManager())
)
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(Customizer.withDefaults())
.logout(logoutConfigurer -> {
logoutConfigurer.addLogoutHandler(oauth2LogoutHandler())
.clearAuthentication(true);
})
.addFilterBefore(new OAuth2TokenEndpointFilter(authenticationManager(),
providerSettings.getTokenEndpoint()),
FilterSecurityInterceptor.class)
.addFilterBefore(new BearerTokenAuthenticationFilter(authenticationManagerResolver()),
LogoutFilter.class)
.addFilterAfter(providerContextFilter, SecurityContextHolderFilter.class)
.sessionManagement(
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.accessDeniedHandler(new JwtAccessDeniedHandler())
);
return http.build();
}
@Bean
Oauth2LogoutHandler oauth2LogoutHandler() {
return new Oauth2LogoutHandler(oauth2AuthorizationService());
}
RequestInfoAuthorizationManager requestInfoAuthorizationManager() {
RoleGetter roleGetter = new DefaultRoleGetter(extensionClient);
return new RequestInfoAuthorizationManager(roleGetter);
}
AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver() {
return new TokenAuthenticationManagerResolver(jwtDecoder(),
personalAccessTokenDecoder());
}
@Bean
AuthenticationManager authenticationManager() {
authenticationManagerBuilder.authenticationProvider(passwordAuthenticationProvider())
.authenticationProvider(oauth2RefreshTokenAuthenticationProvider());
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();
JwtAccessTokenNonBlockedValidator jwtAccessTokenNonBlockedValidator =
new JwtAccessTokenNonBlockedValidator(oauth2AuthorizationService());
OAuth2TokenValidator<Jwt> jwtValidator = new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(),
jwtAccessTokenNonBlockedValidator);
jwtDecoder.setJwtValidator(jwtValidator);
return jwtDecoder;
}
@Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
OAuth2AuthorizationService oauth2AuthorizationService() {
return new InMemoryOAuth2AuthorizationService();
}
@Bean
OAuth2PasswordAuthenticationProvider passwordAuthenticationProvider() {
OAuth2PasswordAuthenticationProvider authenticationProvider =
new OAuth2PasswordAuthenticationProvider(jwtGenerator(), oauth2AuthorizationService());
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
@Bean
OAuth2RefreshTokenAuthenticationProvider oauth2RefreshTokenAuthenticationProvider() {
return new OAuth2RefreshTokenAuthenticationProvider(oauth2AuthorizationService(),
jwtGenerator());
}
@Bean
JwtGenerator jwtGenerator() {
return new JwtGenerator(jwtEncoder());
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
// TODO fake role and role bindings, only used for testing/development
// It'll be deleted next time
UserDetails user = User.withUsername("user")
.password(passwordEncoder().encode("123456"))
.authorities("role-template-view-posts", "role-template-manage-posts")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
ProviderSettings providerSettings() {
return ProviderSettings.builder().build();
}
}

View File

@ -0,0 +1,121 @@
package run.halo.app.config;
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.infra.properties.JwtProperties;
import run.halo.app.security.authentication.jwt.LoginAuthenticationFilter;
import run.halo.app.security.authentication.jwt.LoginAuthenticationManager;
import run.halo.app.security.authorization.RequestInfoAuthorizationManager;
import run.halo.app.security.authorization.RoleGetter;
/**
* Security configuration for WebFlux.
*
* @author johnniang
*/
@EnableWebFluxSecurity
public class WebServerSecurityConfig {
private final JwtProperties jwtProp;
public WebServerSecurityConfig(JwtProperties jwtProp) {
this.jwtProp = jwtProp;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http,
ServerCodecConfigurer codec,
ServerResponse.Context context,
RoleGetter roleGetter) {
http.csrf().disable()
.securityMatcher(pathMatchers("/api/**", "/apis/**"))
.authorizeExchange(exchanges ->
exchanges.anyExchange().access(new RequestInfoAuthorizationManager(roleGetter)))
// for reuse the JWT authentication
.oauth2ResourceServer().jwt();
var loginManager = new LoginAuthenticationManager(userDetailsService(), passwordEncoder());
var loginFilter = new LoginAuthenticationFilter(loginManager,
codec,
jwtEncoder(),
jwtProp,
context);
http.addFilterAt(loginFilter, SecurityWebFiltersOrder.FORM_LOGIN);
return http.build();
}
@Bean
SecurityWebFilterChain webFilterChain(ServerHttpSecurity http) {
http.authorizeExchange(
exchanges -> exchanges.pathMatchers("/v3/api-docs/**", "/v3/api-docs.yaml",
"/swagger-ui/**", "/swagger-ui.html", "/webjars/**").permitAll())
.authorizeExchange(exchanges -> exchanges.anyExchange().authenticated())
.cors(withDefaults()).httpBasic(withDefaults()).formLogin(withDefaults())
.logout(withDefaults());
return http.build();
}
@Bean
ReactiveUserDetailsService userDetailsService() {
//TODO Implement details service when User Extension is ready.
return new MapReactiveUserDetailsService(
// for test
User.withDefaultPasswordEncoder().username("user").password("password").roles("USER")
.build(),
// for test
User.withDefaultPasswordEncoder().username("admin").password("password").roles("ADMIN")
.build());
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
ReactiveJwtDecoder jwtDecoder() {
return new SupplierReactiveJwtDecoder(
() -> NimbusReactiveJwtDecoder.withPublicKey(jwtProp.getPublicKey())
.signatureAlgorithm(jwtProp.getJwsAlgorithm())
.build());
}
@Bean
JwtEncoder jwtEncoder() {
var rsaKey = new RSAKey.Builder(jwtProp.getPublicKey())
.privateKey(jwtProp.getPrivateKey())
.algorithm(JWSAlgorithm.parse(jwtProp.getJwsAlgorithm().getName()))
.build();
var jwks = new ImmutableJWKSet<>(new JWKSet(rsaKey));
return new NimbusJwtEncoder(jwks);
}
}

View File

@ -1,29 +0,0 @@
package run.halo.app.identity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* A controller should ONLY be used during testing for this PR.
* TODO It'll be deleted next time
*
* @author guqing
* @since 2.0.0
*/
@Controller
@ResponseBody
@RequestMapping
public class HealthyController {
@RequestMapping("/healthy")
public String hello() {
return "I am very healthy.";
}
@GetMapping("/static/js/test.js")
public String fakeJs() {
return "console.log('hello world!')";
}
}

View File

@ -1,53 +0,0 @@
package run.halo.app.identity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* A controller should ONLY be used during testing for this PR.
* TODO It'll be deleted next time
*
* @author guqing
* @since 2.0.0
*/
@RestController
@RequestMapping
public class TestController {
@GetMapping("/posts")
public String hello() {
return "list posts.";
}
@GetMapping("/posts/{name}")
public String getPostByName(@PathVariable String name) {
return "Gets a post with the name: " + name;
}
@GetMapping("/categories/{name}")
public String getCategoryByName(@PathVariable String name) {
return "Gets a category with the name: " + name;
}
@GetMapping("/categories")
public String listCategories() {
return "list categories.";
}
@GetMapping("/tags")
public String tags() {
return "list tags";
}
@GetMapping("/{name}")
public String getByName(@PathVariable String name) {
return "Gets a tag with the name: " + name;
}
@RequestMapping("/healthy")
public String healthy() {
return "That's very healthy";
}
}

View File

@ -1,114 +0,0 @@
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());
}
}

View File

@ -1,60 +0,0 @@
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);
}
}

View File

@ -1,75 +0,0 @@
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;
}
}

View File

@ -1,10 +0,0 @@
package run.halo.app.identity.apitoken;
/**
* @author guqing
* @since 2.0.0
*/
public interface PersonalAccessTokenDecoder {
PersonalAccessToken decode(String token) throws PersonalAccessTokenException;
}

View File

@ -1,18 +0,0 @@
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);
}
}

View File

@ -1,76 +0,0 @@
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);
}
}

View File

@ -1,86 +0,0 @@
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;
}
}

View File

@ -1,24 +0,0 @@
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");
}
}

View File

@ -1,102 +0,0 @@
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());
}
}

View File

@ -1,66 +0,0 @@
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);
}
}

View File

@ -1,45 +0,0 @@
package run.halo.app.identity.authentication;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A facility for holding information associated to a specific context.
*
* @author guqing
* @since 2.0.0
*/
public interface Context {
/**
* Returns the value of the attribute associated to the key.
*
* @param key the key for the attribute
* @param <V> the type of the value for the attribute
* @return the value of the attribute associated to the key, or {@code null} if not available
*/
@Nullable
<V> V get(Object key);
/**
* Returns the value of the attribute associated to the key.
*
* @param key the key for the attribute
* @param <V> the type of the value for the attribute
* @return the value of the attribute associated to the key, or {@code null} if not available
* or not of the specified type
*/
@Nullable
default <V> V get(Class<V> key) {
Assert.notNull(key, "key cannot be null");
V value = get((Object) key);
return key.isInstance(value) ? value : null;
}
/**
* Returns {@code true} if an attribute associated to the key exists, {@code false} otherwise.
*
* @param key the key for the attribute
* @return {@code true} if an attribute associated to the key exists, {@code false} otherwise
*/
boolean hasKey(Object key);
}

View File

@ -1,57 +0,0 @@
package run.halo.app.identity.authentication;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Default implementation of {@link OAuth2TokenContext}.
*
* @author guqing
* @since 2.0.0
*/
public record DefaultOAuth2TokenContext(Map<Object, Object> context) implements OAuth2TokenContext {
public DefaultOAuth2TokenContext {
context = Map.copyOf(context);
}
@SuppressWarnings("unchecked")
@Nullable
@Override
public <V> V get(Object key) {
return hasKey(key) ? (V) this.context.get(key) : null;
}
@Override
public boolean hasKey(Object key) {
Assert.notNull(key, "key cannot be null");
return this.context.containsKey(key);
}
/**
* Returns a new {@link Builder}.
*
* @return the {@link Builder}
*/
public static Builder builder() {
return new Builder();
}
/**
* A builder for {@link DefaultOAuth2TokenContext}.
*/
public static final class Builder extends AbstractBuilder<DefaultOAuth2TokenContext, Builder> {
private Builder() {
}
/**
* Builds a new {@link DefaultOAuth2TokenContext}.
*
* @return the {@link DefaultOAuth2TokenContext}
*/
public DefaultOAuth2TokenContext build() {
return new DefaultOAuth2TokenContext(getContext());
}
}
}

View File

@ -1,49 +0,0 @@
package run.halo.app.identity.authentication;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationConverter} that simply delegates to it's
* internal {@code List} of {@link AuthenticationConverter}(s).
* <p>
* Each {@link AuthenticationConverter} is given a chance to
* {@link AuthenticationConverter#convert(HttpServletRequest)}
* with the first {@code non-null} {@link Authentication} being returned.
*
* @author guqing
* @see AuthenticationConverter
* @since 2.0.0
*/
public class DelegatingAuthenticationConverter implements AuthenticationConverter {
private final List<AuthenticationConverter> converters;
/**
* Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters.
*
* @param converters a {@code List} of {@link AuthenticationConverter}(s)
*/
public DelegatingAuthenticationConverter(List<AuthenticationConverter> converters) {
Assert.notEmpty(converters, "converters cannot be empty");
this.converters = Collections.unmodifiableList(new LinkedList<>(converters));
}
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
Assert.notNull(request, "request cannot be null");
for (AuthenticationConverter converter : this.converters) {
Authentication authentication = converter.convert(request);
if (authentication != null) {
return authentication;
}
}
return null;
}
}

View File

@ -1,169 +0,0 @@
package run.halo.app.identity.authentication;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.util.Assert;
/**
* An {@link OAuth2AuthorizationService} that stores {@link OAuth2Authorization}'s in-memory.<p>
* <b>NOTE:</b> This implementation should ONLY be used during development/testing.
*
* @author guqing
* @see OAuth2AuthorizationService
* @since 2.0.0
*/
public class InMemoryOAuth2AuthorizationService implements OAuth2AuthorizationService {
private int maxInitializedAuthorizations = 100;
/*
* Stores "initialized" (uncompleted) authorizations, where an access token has not yet been
* granted.
* This state occurs with the authorization_code grant flow during the user consent step OR
* when the code is returned in the authorization response but the access token request is
* not yet initiated.
*/
private Map<String, OAuth2Authorization> initializedAuthorizations =
Collections.synchronizedMap(new MaxSizeHashMap<>(this.maxInitializedAuthorizations));
/*
* Stores "completed" authorizations, where an access token has been granted.
*/
private final Map<String, OAuth2Authorization> authorizations = new ConcurrentHashMap<>();
/*
* Constructor used for testing only.
*/
public InMemoryOAuth2AuthorizationService(int maxInitializedAuthorizations) {
this.maxInitializedAuthorizations = maxInitializedAuthorizations;
this.initializedAuthorizations =
Collections.synchronizedMap(new MaxSizeHashMap<>(this.maxInitializedAuthorizations));
}
/**
* Constructs an {@code InMemoryOAuth2AuthorizationService}.
*/
public InMemoryOAuth2AuthorizationService() {
this(Collections.emptyList());
}
/**
* Constructs an {@code InMemoryOAuth2AuthorizationService} using the provided parameters.
*
* @param authorizations the authorization(s)
*/
public InMemoryOAuth2AuthorizationService(OAuth2Authorization... authorizations) {
this(Arrays.asList(authorizations));
}
/**
* Constructs an {@code InMemoryOAuth2AuthorizationService} using the provided parameters.
*
* @param authorizations the authorization(s)
*/
public InMemoryOAuth2AuthorizationService(List<OAuth2Authorization> authorizations) {
Assert.notNull(authorizations, "authorizations cannot be null");
authorizations.forEach(authorization -> {
Assert.notNull(authorization, "authorization cannot be null");
Assert.isTrue(!this.authorizations.containsKey(authorization.getId()),
"The authorization must be unique. Found duplicate identifier: "
+ authorization.getId());
this.authorizations.put(authorization.getId(), authorization);
});
}
@Override
public void save(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
if (isComplete(authorization)) {
this.authorizations.put(authorization.getId(), authorization);
} else {
this.initializedAuthorizations.put(authorization.getId(), authorization);
}
}
@Override
public void remove(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
if (isComplete(authorization)) {
this.authorizations.remove(authorization.getId(), authorization);
} else {
this.initializedAuthorizations.remove(authorization.getId(), authorization);
}
}
@Nullable
@Override
public OAuth2Authorization findById(String id) {
Assert.hasText(id, "id cannot be empty");
OAuth2Authorization authorization = this.authorizations.get(id);
return authorization != null
? authorization :
this.initializedAuthorizations.get(id);
}
@Nullable
@Override
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
Assert.hasText(token, "token cannot be empty");
for (OAuth2Authorization authorization : this.authorizations.values()) {
if (hasToken(authorization, token, tokenType)) {
return authorization;
}
}
for (OAuth2Authorization authorization : this.initializedAuthorizations.values()) {
if (hasToken(authorization, token, tokenType)) {
return authorization;
}
}
return null;
}
private static boolean isComplete(OAuth2Authorization authorization) {
return authorization.getAccessToken() != null;
}
private static boolean hasToken(OAuth2Authorization authorization, String token,
@Nullable OAuth2TokenType tokenType) {
if (tokenType == null) {
return matchesAccessToken(authorization, token)
|| matchesRefreshToken(authorization, token);
} else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
return matchesAccessToken(authorization, token);
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
return matchesRefreshToken(authorization, token);
}
return false;
}
private static boolean matchesAccessToken(OAuth2Authorization authorization, String token) {
OAuth2Authorization.Token<OAuth2AccessToken> accessToken =
authorization.getToken(OAuth2AccessToken.class);
return accessToken != null && accessToken.getToken().getTokenValue().equals(token);
}
private static boolean matchesRefreshToken(OAuth2Authorization authorization, String token) {
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken =
authorization.getToken(OAuth2RefreshToken.class);
return refreshToken != null && refreshToken.getToken().getTokenValue().equals(token);
}
private static final class MaxSizeHashMap<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
private MaxSizeHashMap(int maxSize) {
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > this.maxSize;
}
}
}

View File

@ -1,83 +0,0 @@
package run.halo.app.identity.authentication;
import java.time.Instant;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwsHeader;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import run.halo.app.infra.utils.HaloUtils;
/**
* An {@link OAuth2TokenGenerator} that generates a {@link Jwt}
* used for an {@link OAuth2AccessToken}.
*
* @author guqing
* @see OAuth2TokenGenerator
* @see Jwt
* @see JwtEncoder
* @see OAuth2AccessToken
* @since 2.0.0
*/
public record JwtGenerator(JwtEncoder jwtEncoder) implements OAuth2TokenGenerator<Jwt> {
/**
* Constructs a {@code JwtGenerator} using the provided parameters.
*
* @param jwtEncoder the jwt encoder
*/
public JwtGenerator {
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
}
@Nullable
@Override
public Jwt generate(OAuth2TokenContext context) {
if (context.getTokenType() == null
|| (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())
&& !OAuth2TokenType.REFRESH_TOKEN.equals(context.getTokenType()))) {
return null;
}
Instant issuedAt = Instant.now();
ProviderSettings providerSettings = context.getProviderContext().providerSettings();
Instant expiresAt;
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
expiresAt = issuedAt.plus(providerSettings.getAccessTokenTimeToLive());
} else {
// refresh token
expiresAt = issuedAt.plus(providerSettings.getRefreshTokenTimeToLive());
}
String issuer = null;
if (context.getProviderContext() != null) {
issuer = context.getProviderContext().getIssuer();
}
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();
if (StringUtils.hasText(issuer)) {
claimsBuilder.issuer(issuer);
}
claimsBuilder
.subject(context.getPrincipal().getName())
.issuedAt(issuedAt)
.notBefore(issuedAt)
.id(HaloUtils.simpleUUID())
.expiresAt(expiresAt);
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
claimsBuilder.notBefore(issuedAt);
if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
}
}
JwsHeader headers = JwsHeader.with(SignatureAlgorithm.RS256).build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(headers, claimsBuilder.build()));
}
}

View File

@ -1,103 +0,0 @@
package run.halo.app.identity.authentication;
import java.util.Collections;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.util.Assert;
/**
* @author guqing
* @since 2.0
*/
public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthenticationToken {
private final Authentication principal;
private final OAuth2AccessToken accessToken;
private final OAuth2RefreshToken refreshToken;
private final Map<String, Object> additionalParameters;
/**
* Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided parameters.
*
* @param clientPrincipal the authenticated client principal
* @param accessToken the access token
*/
public OAuth2AccessTokenAuthenticationToken(Authentication clientPrincipal,
OAuth2AccessToken accessToken) {
this(clientPrincipal, accessToken, null);
}
/**
* Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided parameters.
*
* @param clientPrincipal the authenticated client principal
* @param accessToken the access token
* @param refreshToken the refresh token
*/
public OAuth2AccessTokenAuthenticationToken(Authentication clientPrincipal,
OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) {
this(clientPrincipal, accessToken, refreshToken, Collections.emptyMap());
}
/**
* Constructs an {@code OAuth2AccessTokenAuthenticationToken} using the provided parameters.
*
* @param principal the authenticated principal
* @param accessToken the access token
* @param refreshToken the refresh token
* @param additionalParameters the additional parameters
*/
public OAuth2AccessTokenAuthenticationToken(Authentication principal,
OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken,
Map<String, Object> additionalParameters) {
super(Collections.emptyList());
Assert.notNull(principal, "principal cannot be null");
Assert.notNull(accessToken, "accessToken cannot be null");
Assert.notNull(additionalParameters, "additionalParameters cannot be null");
this.principal = principal;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.additionalParameters = additionalParameters;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the {@link OAuth2AccessToken access token}.
*
* @return the {@link OAuth2AccessToken}
*/
public OAuth2AccessToken getAccessToken() {
return this.accessToken;
}
/**
* Returns the {@link OAuth2RefreshToken refresh token}.
*
* @return the {@link OAuth2RefreshToken} or {@code null} if not available
*/
@Nullable
public OAuth2RefreshToken getRefreshToken() {
return this.refreshToken;
}
/**
* Returns the additional parameters.
*
* @return a {@code Map} of the additional parameters, may be empty
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
}

View File

@ -1,478 +0,0 @@
package run.halo.app.identity.authentication;
import java.io.Serializable;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* @author guqing
* @since 2.0
*/
public class OAuth2Authorization implements Serializable {
/**
* The name of the {@link #getAttribute(String) attribute} used for the authorized scope(s).
* The value of the attribute is of type {@code Set<String>}.
*/
public static final String AUTHORIZED_SCOPE_ATTRIBUTE_NAME =
OAuth2Authorization.class.getName().concat(".AUTHORIZED_SCOPE");
private String id;
private String principalName;
private AuthorizationGrantType authorizationGrantType;
private Map<Class<? extends OAuth2Token>, Token<?>> tokens;
private Map<String, Object> attributes;
protected OAuth2Authorization() {
}
/**
* Returns the identifier for the authorization.
*
* @return the identifier for the authorization
*/
public String getId() {
return this.id;
}
/**
* Returns the {@code Principal} name of the resource owner (or client).
*
* @return the {@code Principal} name of the resource owner (or client)
*/
public String getPrincipalName() {
return this.principalName;
}
/**
* Returns the {@link AuthorizationGrantType authorization grant type} used for the
* authorization.
*
* @return the {@link AuthorizationGrantType} used for the authorization
*/
public AuthorizationGrantType getAuthorizationGrantType() {
return this.authorizationGrantType;
}
/**
* Returns the {@link Token} of type {@link OAuth2AccessToken}.
*
* @return the {@link Token} of type {@link OAuth2AccessToken}
*/
public Token<OAuth2AccessToken> getAccessToken() {
return getToken(OAuth2AccessToken.class);
}
/**
* Returns the {@link Token} of type {@link OAuth2RefreshToken}.
*
* @return the {@link Token} of type {@link OAuth2RefreshToken}, or {@code null} if not
* available
*/
@Nullable
public Token<OAuth2RefreshToken> getRefreshToken() {
return getToken(OAuth2RefreshToken.class);
}
/**
* Returns the {@link Token} of type {@code tokenType}.
*
* @param tokenType the token type
* @param <T> the type of the token
* @return the {@link Token}, or {@code null} if not available
*/
@Nullable
@SuppressWarnings("unchecked")
public <T extends OAuth2Token> Token<T> getToken(Class<T> tokenType) {
Assert.notNull(tokenType, "tokenType cannot be null");
Token<?> token = this.tokens.get(tokenType);
return token != null ? (Token<T>) token : null;
}
/**
* Returns the {@link Token} matching the {@code tokenValue}.
*
* @param tokenValue the token value
* @param <T> the type of the token
* @return the {@link Token}, or {@code null} if not available
*/
@Nullable
@SuppressWarnings("unchecked")
public <T extends OAuth2Token> Token<T> getToken(String tokenValue) {
Assert.hasText(tokenValue, "tokenValue cannot be empty");
for (Token<?> token : this.tokens.values()) {
if (token.getToken().getTokenValue().equals(tokenValue)) {
return (Token<T>) token;
}
}
return null;
}
/**
* Returns the attribute(s) associated to the authorization.
*
* @return a {@code Map} of the attribute(s)
*/
public Map<String, Object> getAttributes() {
return this.attributes;
}
/**
* Returns the value of an attribute associated to the authorization.
*
* @param name the name of the attribute
* @param <T> the type of the attribute
* @return the value of an attribute associated to the authorization, or {@code null} if not
* available
*/
@Nullable
@SuppressWarnings("unchecked")
public <T> T getAttribute(String name) {
Assert.hasText(name, "name cannot be empty");
return (T) this.attributes.get(name);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
OAuth2Authorization that = (OAuth2Authorization) obj;
return Objects.equals(this.id, that.id)
&& Objects.equals(this.principalName, that.principalName)
&& Objects.equals(this.tokens, that.tokens)
&& Objects.equals(this.attributes, that.attributes);
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.principalName, this.tokens, this.attributes);
}
/**
* Returns a new {@link Builder}, initialized with the values from the provided {@code
* OAuth2Authorization}.
*
* @param authorization the {@code OAuth2Authorization} used for initializing the
* {@link Builder}
* @return the {@link Builder}
*/
public static Builder from(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
return new Builder()
.id(authorization.getId())
.principalName(authorization.getPrincipalName())
.tokens(authorization.tokens)
.attributes(attrs -> attrs.putAll(authorization.getAttributes()));
}
/**
* A holder of an OAuth 2.0 Token and it's associated metadata.
*
* @author guqing
* @since 2.0
*/
public static class Token<T extends OAuth2Token> implements Serializable {
protected static final String TOKEN_METADATA_NAMESPACE = "metadata.token.";
/**
* The name of the metadata that indicates if the token has been invalidated.
*/
public static final String INVALIDATED_METADATA_NAME =
TOKEN_METADATA_NAMESPACE.concat("invalidated");
/**
* The name of the metadata used for the claims of the token.
*/
public static final String CLAIMS_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("claims");
private final T token;
private final Map<String, Object> metadata;
protected Token(T token) {
this(token, defaultMetadata());
}
protected Token(T token, Map<String, Object> metadata) {
this.token = token;
this.metadata = Collections.unmodifiableMap(metadata);
}
/**
* Returns the token of type {@link OAuth2Token}.
*
* @return the token of type {@link OAuth2Token}
*/
public T getToken() {
return this.token;
}
/**
* Returns {@code true} if the token has been invalidated (e.g. revoked).
* The default is {@code false}.
*
* @return {@code true} if the token has been invalidated, {@code false} otherwise
*/
public boolean isInvalidated() {
return Boolean.TRUE.equals(getMetadata(INVALIDATED_METADATA_NAME));
}
/**
* Returns {@code true} if the token has expired.
*
* @return {@code true} if the token has expired, {@code false} otherwise
*/
public boolean isExpired() {
return getToken().getExpiresAt() != null && Instant.now()
.isAfter(getToken().getExpiresAt());
}
/**
* Returns {@code true} if the token is before the time it can be used.
*
* @return {@code true} if the token is before the time it can be used, {@code false}
* otherwise
*/
public boolean isBeforeUse() {
Instant notBefore = null;
if (!CollectionUtils.isEmpty(getClaims())) {
notBefore = (Instant) getClaims().get("nbf");
}
return notBefore != null && Instant.now().isBefore(notBefore);
}
/**
* Returns {@code true} if the token is currently active.
*
* @return {@code true} if the token is currently active, {@code false} otherwise
*/
public boolean isActive() {
return !isInvalidated() && !isExpired() && !isBeforeUse();
}
/**
* Returns the claims associated to the token.
*
* @return a {@code Map} of the claims, or {@code null} if not available
*/
@Nullable
public Map<String, Object> getClaims() {
return getMetadata(CLAIMS_METADATA_NAME);
}
/**
* Returns the value of the metadata associated to the token.
*
* @param name the name of the metadata
* @param <V> the value type of the metadata
* @return the value of the metadata, or {@code null} if not available
*/
@Nullable
@SuppressWarnings("unchecked")
public <V> V getMetadata(String name) {
Assert.hasText(name, "name cannot be empty");
return (V) this.metadata.get(name);
}
/**
* Returns the metadata associated to the token.
*
* @return a {@code Map} of the metadata
*/
public Map<String, Object> getMetadata() {
return this.metadata;
}
protected static Map<String, Object> defaultMetadata() {
Map<String, Object> metadata = new HashMap<>();
metadata.put(INVALIDATED_METADATA_NAME, false);
return metadata;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Token<?> that = (Token<?>) obj;
return Objects.equals(this.token, that.token)
&& Objects.equals(this.metadata, that.metadata);
}
@Override
public int hashCode() {
return Objects.hash(this.token, this.metadata);
}
}
/**
* A builder for {@link OAuth2Authorization}.
*/
public static class Builder implements Serializable {
private String id;
private String principalName;
private AuthorizationGrantType authorizationGrantType;
private Map<Class<? extends OAuth2Token>, Token<?>> tokens = new HashMap<>();
private final Map<String, Object> attributes = new HashMap<>();
/**
* Sets the identifier for the authorization.
*
* @param id the identifier for the authorization
* @return the {@link Builder}
*/
public Builder id(String id) {
this.id = id;
return this;
}
/**
* Sets the {@code Principal} name of the resource owner (or client).
*
* @param principalName the {@code Principal} name of the resource owner (or client)
* @return the {@link Builder}
*/
public Builder principalName(String principalName) {
this.principalName = principalName;
return this;
}
/**
* Sets the {@link AuthorizationGrantType authorization grant type} used for the
* authorization.
*
* @param authorizationGrantType the {@link AuthorizationGrantType}
* @return the {@link Builder}
*/
public Builder authorizationGrantType(AuthorizationGrantType authorizationGrantType) {
this.authorizationGrantType = authorizationGrantType;
return this;
}
/**
* Sets the {@link OAuth2AccessToken access token}.
*
* @param accessToken the {@link OAuth2AccessToken}
* @return the {@link Builder}
*/
public Builder accessToken(OAuth2AccessToken accessToken) {
return token(accessToken);
}
/**
* Sets the {@link OAuth2RefreshToken refresh token}.
*
* @param refreshToken the {@link OAuth2RefreshToken}
* @return the {@link Builder}
*/
public Builder refreshToken(OAuth2RefreshToken refreshToken) {
return token(refreshToken);
}
/**
* Sets the {@link OAuth2Token token}.
*
* @param token the token
* @param <T> the type of the token
* @return the {@link Builder}
*/
public <T extends OAuth2Token> Builder token(T token) {
return token(token, (metadata) -> {
});
}
/**
* Sets the {@link OAuth2Token token} and associated metadata.
*
* @param token the token
* @param metadataConsumer a {@code Consumer} of the metadata {@code Map}
* @param <T> the type of the token
* @return the {@link Builder}
*/
public <T extends OAuth2Token> Builder token(T token,
Consumer<Map<String, Object>> metadataConsumer) {
Assert.notNull(token, "token cannot be null");
Map<String, Object> metadata = Token.defaultMetadata();
Token<?> existingToken = this.tokens.get(token.getClass());
if (existingToken != null) {
metadata.putAll(existingToken.getMetadata());
}
metadataConsumer.accept(metadata);
Class<? extends OAuth2Token> tokenClass = token.getClass();
this.tokens.put(tokenClass, new Token<>(token, metadata));
return this;
}
protected final Builder tokens(Map<Class<? extends OAuth2Token>, Token<?>> tokens) {
this.tokens = new HashMap<>(tokens);
return this;
}
/**
* Adds an attribute associated to the authorization.
*
* @param name the name of the attribute
* @param value the value of the attribute
* @return the {@link Builder}
*/
public Builder attribute(String name, Object value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
this.attributes.put(name, value);
return this;
}
/**
* A {@code Consumer} of the attributes {@code Map}
* allowing the ability to add, replace, or remove.
*
* @param attributesConsumer a {@link Consumer} of the attributes {@code Map}
* @return the {@link Builder}
*/
public Builder attributes(Consumer<Map<String, Object>> attributesConsumer) {
attributesConsumer.accept(this.attributes);
return this;
}
/**
* Builds a new {@link OAuth2Authorization}.
*
* @return the {@link OAuth2Authorization}
*/
public OAuth2Authorization build() {
Assert.hasText(this.principalName, "principalName cannot be empty");
OAuth2Authorization authorization = new OAuth2Authorization();
if (!StringUtils.hasText(this.id)) {
this.id = UUID.randomUUID().toString();
}
authorization.id = this.id;
authorization.principalName = this.principalName;
authorization.authorizationGrantType = this.authorizationGrantType;
authorization.tokens = Collections.unmodifiableMap(this.tokens);
authorization.attributes = Collections.unmodifiableMap(this.attributes);
return authorization;
}
}
}

View File

@ -1,71 +0,0 @@
package run.halo.app.identity.authentication;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
/**
* Base implementation of an {@link Authentication} representing an OAuth 2.0 Authorization Grant.
*
* @author guqing
* @see AbstractAuthenticationToken
* @see AuthorizationGrantType
* @see
* <a href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
* @since 2.0.0
*/
public class OAuth2AuthorizationGrantAuthenticationToken extends AbstractAuthenticationToken {
private final AuthorizationGrantType authorizationGrantType;
private final Map<String, Object> additionalParameters;
/**
* Sub-class constructor.
*
* @param authorizationGrantType the authorization grant type
* @param additionalParameters the additional parameters
*/
protected OAuth2AuthorizationGrantAuthenticationToken(
AuthorizationGrantType authorizationGrantType,
@Nullable Map<String, Object> additionalParameters) {
super(Collections.emptyList());
Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
this.authorizationGrantType = authorizationGrantType;
this.additionalParameters = Collections.unmodifiableMap(
additionalParameters != null
? new HashMap<>(additionalParameters) :
Collections.emptyMap());
}
/**
* Returns the authorization grant type.
*
* @return the authorization grant type
*/
public AuthorizationGrantType getGrantType() {
return this.authorizationGrantType;
}
@Override
public Object getPrincipal() {
return "";
}
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the additional parameters.
*
* @return the additional parameters
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
}

View File

@ -1,45 +0,0 @@
package run.halo.app.identity.authentication;
import org.springframework.lang.Nullable;
/**
* @author guqing
* @since 2.0.0
*/
public interface OAuth2AuthorizationService {
/**
* Returns the {@link OAuth2Authorization} containing the provided {@code token},
* or {@code null} if not found.
*
* @param token the token credential
* @param tokenType the {@link OAuth2TokenType token type}
* @return the {@link OAuth2Authorization} if found, otherwise {@code null}
*/
@Nullable
OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType);
/**
* Saves the {@link OAuth2Authorization}.
*
* @param authorization the {@link OAuth2Authorization}
*/
void save(OAuth2Authorization authorization);
/**
* Removes the {@link OAuth2Authorization}.
*
* @param authorization the {@link OAuth2Authorization}
*/
void remove(OAuth2Authorization authorization);
/**
* Returns the {@link OAuth2Authorization} identified by the provided {@code id},
* or {@code null} if not found.
*
* @param id the authorization identifier
* @return the {@link OAuth2Authorization} if found, otherwise {@code null}
*/
@Nullable
OAuth2Authorization findById(String id);
}

View File

@ -1,36 +0,0 @@
package run.halo.app.identity.authentication;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* @author guqing
* @since 2.0.0
*/
public class OAuth2EndpointUtils {
static final String ERROR_URI =
"https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
parameters.add(key, value);
}
}
});
return parameters;
}
static void throwError(String errorCode, String parameterName, String errorUri) {
OAuth2Error
error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthenticationException(error);
}
}

View File

@ -1,75 +0,0 @@
package run.halo.app.identity.authentication;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* @author guqing
* @since 2.0.0
*/
public class OAuth2PasswordAuthenticationConverter implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {
return null;
}
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.SCOPE,
OAuth2EndpointUtils.ERROR_URI);
}
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (!StringUtils.hasText(username)
|| parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.USERNAME,
OAuth2EndpointUtils.ERROR_URI);
}
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (!StringUtils.hasText(password)) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.PASSWORD,
OAuth2EndpointUtils.ERROR_URI);
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE)
&& !key.equals(OAuth2ParameterNames.USERNAME)
&& !key.equals(OAuth2ParameterNames.SCOPE)
&& !key.equals(OAuth2ParameterNames.PASSWORD)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2PasswordAuthenticationToken(username, password, requestedScopes,
additionalParameters);
}
}

View File

@ -1,138 +0,0 @@
package run.halo.app.identity.authentication;
import java.security.Principal;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Password Grant.
*
* @author guqing
* @see OAuth2PasswordAuthenticationProvider
* @see OAuth2AuthorizationService
* @see OAuth2TokenGenerator
* @see
* <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.3">Section-4.3 Password Credentials Grant</a>
* @since 2.0.0
*/
public class OAuth2PasswordAuthenticationProvider extends DaoAuthenticationProvider
implements AuthenticationProvider {
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
public OAuth2PasswordAuthenticationProvider(
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
OAuth2PasswordAuthenticationToken passwordAuthentication =
(OAuth2PasswordAuthenticationToken) authentication;
// Convert to UsernamePasswordAuthenticationToken type
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(passwordAuthentication.getUsername(),
passwordAuthentication.getPassword());
// and call the authenticate method of the super.
// then super#authenticate() method will call this#createSuccessAuthentication()
return super.authenticate(authenticationToken);
}
@Override
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// convert to UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
(UsernamePasswordAuthenticationToken) super.createSuccessAuthentication(principal,
authentication, user);
Set<String> scopes = usernamePasswordAuthenticationToken.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.principal(authentication)
.providerContext(ProviderContextHolder.getProviderContext())
.authorizedScopes(scopes);
OAuth2Authorization.Builder authorizationBuilder = new OAuth2Authorization.Builder()
.principalName(authentication.getName())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, scopes)
.attribute(Principal.class.getName(), authentication);
// ----- Access token -----
OAuth2TokenContext tokenContext =
tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.",
OAuth2EndpointUtils.ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> {
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME,
((ClaimAccessor) generatedAccessToken).getClaims());
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false);
});
} else {
authorizationBuilder.accessToken(accessToken);
}
ProviderSettings providerSettings =
ProviderContextHolder.getProviderContext().providerSettings();
// ----- Refresh token -----
OAuth2RefreshToken currentRefreshToken = null;
if (!providerSettings.isReuseRefreshTokens()) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (generatedRefreshToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.",
OAuth2EndpointUtils.ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
currentRefreshToken = new OAuth2RefreshToken(
generatedRefreshToken.getTokenValue(), generatedRefreshToken.getIssuedAt(),
generatedRefreshToken.getExpiresAt());
authorizationBuilder.refreshToken(currentRefreshToken);
}
this.authorizationService.save(authorizationBuilder.build());
return new OAuth2AccessTokenAuthenticationToken(authentication, accessToken,
currentRefreshToken, Collections.emptyMap());
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2PasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@ -1,37 +0,0 @@
package run.halo.app.identity.authentication;
import java.util.Map;
import java.util.Set;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
/**
* @author guqing
* @since 2.0.0
*/
public class OAuth2PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
private final String username;
private final String password;
private final Set<String> scopes;
public OAuth2PasswordAuthenticationToken(String username, String password,
Set<String> scopes, Map<String, Object> additionalParameters) {
super(AuthorizationGrantType.PASSWORD, additionalParameters);
this.username = username;
this.password = password;
this.scopes = scopes;
}
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
public Set<String> getScopes() {
return scopes;
}
}

View File

@ -1,79 +0,0 @@
package run.halo.app.identity.authentication;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Attempts to extract an Access Token Request from {@link HttpServletRequest} for the OAuth 2.0
* Refresh Token Grant
* and then converts it to an {@link OAuth2RefreshTokenAuthenticationToken} used for
* authenticating the authorization grant.
*
* @author guqing
* @see AuthenticationConverter
* @see OAuth2RefreshTokenAuthenticationToken
* @see OAuth2TokenEndpointFilter
* @since 2.0.0
*/
public class OAuth2RefreshTokenAuthenticationConverter implements AuthenticationConverter {
@Nullable
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) {
return null;
}
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// refresh_token (REQUIRED)
String refreshToken = parameters.getFirst(OAuth2ParameterNames.REFRESH_TOKEN);
if (!StringUtils.hasText(refreshToken)
|| parameters.get(OAuth2ParameterNames.REFRESH_TOKEN).size() != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.REFRESH_TOKEN,
OAuth2EndpointUtils.ERROR_URI);
}
// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope)
&& parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.SCOPE,
OAuth2EndpointUtils.ERROR_URI);
}
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE)
&& !key.equals(OAuth2ParameterNames.REFRESH_TOKEN)
&& !key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, value.get(0));
}
});
return new OAuth2RefreshTokenAuthenticationToken(refreshToken, requestedScopes,
additionalParameters);
}
}

View File

@ -1,152 +0,0 @@
package run.halo.app.identity.authentication;
import java.security.Principal;
import java.util.Collections;
import java.util.Set;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Refresh Token Grant.
*
* @author guqing
* @see OAuth2RefreshTokenAuthenticationToken
* @see OAuth2AccessTokenAuthenticationToken
* @see OAuth2AuthorizationService
* @see OAuth2TokenGenerator
* @see
* <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-1.5">Section 1.5 Refresh Token Grant</a>
* @see
* <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
* @since 2.0.0
*/
public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
/**
* Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided parameters.
*
* @param authorizationService the authorization service
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws
AuthenticationException {
OAuth2RefreshTokenAuthenticationToken refreshTokenAuthentication =
(OAuth2RefreshTokenAuthenticationToken) authentication;
OAuth2Authorization authorization = this.authorizationService.findByToken(
refreshTokenAuthentication.getRefreshToken(), OAuth2TokenType.REFRESH_TOKEN);
if (authorization == null) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
OAuth2Authorization.Token<OAuth2RefreshToken> refreshToken =
authorization.getRefreshToken();
if (refreshToken == null || !refreshToken.isActive()) {
// As per https://tools.ietf.org/html/rfc6749#section-5.2
// invalid_grant: The provided authorization grant (e.g., authorization code,
// resource owner credentials) or refresh token is invalid, expired, revoked [...].
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
// As per https://tools.ietf.org/html/rfc6749#section-6
// The requested scope MUST NOT include any scope not originally granted by the resource
// owner,
// and if omitted is treated as equal to the scope originally granted by the resource owner.
Set<String> scopes = refreshTokenAuthentication.getScopes();
Set<String> authorizedScopes =
authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
if (!authorizedScopes.containsAll(scopes)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
if (scopes.isEmpty()) {
scopes = authorizedScopes;
}
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.principal(authorization.getAttribute(Principal.class.getName()))
.providerContext(ProviderContextHolder.getProviderContext())
.authorization(authorization)
.authorizedScopes(scopes)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrant(refreshTokenAuthentication);
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization);
// ----- Access token -----
OAuth2TokenContext tokenContext =
tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.",
OAuth2EndpointUtils.ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> {
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME,
((ClaimAccessor) generatedAccessToken).getClaims());
metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false);
});
} else {
authorizationBuilder.accessToken(accessToken);
}
ProviderSettings providerSettings =
ProviderContextHolder.getProviderContext().providerSettings();
// ----- Refresh token -----
OAuth2RefreshToken currentRefreshToken = null;
if (!providerSettings.isReuseRefreshTokens()) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (generatedRefreshToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.",
OAuth2EndpointUtils.ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
currentRefreshToken = new OAuth2RefreshToken(
generatedRefreshToken.getTokenValue(), generatedRefreshToken.getIssuedAt(),
generatedRefreshToken.getExpiresAt());
authorizationBuilder.refreshToken(currentRefreshToken);
}
authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
return new OAuth2AccessTokenAuthenticationToken(refreshTokenAuthentication, accessToken,
currentRefreshToken, Collections.emptyMap());
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@ -1,53 +0,0 @@
package run.halo.app.identity.authentication;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
/**
* @author guqing
* @since 2.0.0
*/
public class OAuth2RefreshTokenAuthenticationToken
extends OAuth2AuthorizationGrantAuthenticationToken {
private final String refreshToken;
private final Set<String> scopes;
/**
* Constructs an {@code OAuth2RefreshTokenAuthenticationToken} using the provided parameters.
*
* @param refreshToken the refresh token
* @param scopes the requested scope(s)
* @param additionalParameters the additional parameters
*/
public OAuth2RefreshTokenAuthenticationToken(String refreshToken,
@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
super(AuthorizationGrantType.REFRESH_TOKEN, additionalParameters);
Assert.hasText(refreshToken, "refreshToken cannot be empty");
this.refreshToken = refreshToken;
this.scopes = Collections.unmodifiableSet(
scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
}
/**
* Returns the refresh token.
*
* @return the refresh token
*/
public String getRefreshToken() {
return this.refreshToken;
}
/**
* Returns the requested scope(s).
*
* @return the requested scope(s), or an empty {@code Set} if not available
*/
public Set<String> getScopes() {
return this.scopes;
}
}

View File

@ -1,225 +0,0 @@
package run.halo.app.identity.authentication;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
/**
* @author guqing
* @since 2.0.0
*/
public interface OAuth2TokenContext extends Context {
/**
* Returns the {@link Authentication} representing the {@code Principal} resource owner (or
* client).
*
* @param <T> the type of the {@code Authentication}
* @return the {@link Authentication} representing the {@code Principal} resource owner (or
* client)
*/
default <T extends Authentication> T getPrincipal() {
return get(AbstractBuilder.PRINCIPAL_AUTHENTICATION_KEY);
}
/**
* Returns the {@link ProviderContext provider context}.
*
* @return the {@link ProviderContext}
* @since 0.2.3
*/
default ProviderContext getProviderContext() {
return get(ProviderContext.class);
}
/**
* Returns the {@link OAuth2Authorization authorization}.
*
* @return the {@link OAuth2Authorization}, or {@code null} if not available
*/
@Nullable
default OAuth2Authorization getAuthorization() {
return get(OAuth2Authorization.class);
}
/**
* Returns the authorized scope(s).
*
* @return the authorized scope(s)
*/
default Set<String> getAuthorizedScopes() {
return hasKey(AbstractBuilder.AUTHORIZATION_SCOPE_AUTHENTICATION_KEY)
? get(AbstractBuilder.AUTHORIZATION_SCOPE_AUTHENTICATION_KEY) :
Collections.emptySet();
}
/**
* Returns the {@link OAuth2TokenType token type}.
*
* @return the {@link OAuth2TokenType}
*/
default OAuth2TokenType getTokenType() {
return get(OAuth2TokenType.class);
}
/**
* Returns the {@link AuthorizationGrantType authorization grant type}.
*
* @return the {@link AuthorizationGrantType}
*/
default AuthorizationGrantType getAuthorizationGrantType() {
return get(AuthorizationGrantType.class);
}
/**
* Returns the {@link Authentication} representing the authorization grant.
*
* @param <T> the type of the {@code Authentication}
* @return the {@link Authentication} representing the authorization grant
*/
default <T extends Authentication> T getAuthorizationGrant() {
return get(AbstractBuilder.AUTHORIZATION_GRANT_AUTHENTICATION_KEY);
}
/**
* Base builder for implementations of {@link OAuth2TokenContext}.
*
* @param <T> the type of the context
* @param <B> the type of the builder
*/
abstract class AbstractBuilder<T extends OAuth2TokenContext, B extends AbstractBuilder<T, B>> {
private static final String PRINCIPAL_AUTHENTICATION_KEY =
Authentication.class.getName().concat(".PRINCIPAL");
private static final String AUTHORIZATION_SCOPE_AUTHENTICATION_KEY =
Authentication.class.getName().concat(".AUTHORIZED_SCOPE");
private static final String AUTHORIZATION_GRANT_AUTHENTICATION_KEY =
Authentication.class.getName().concat(".AUTHORIZATION_GRANT");
private final Map<Object, Object> context = new HashMap<>();
/**
* Sets the {@link Authentication} representing the {@code Principal} resource owner (or
* client).
*
* @param principal the {@link Authentication} representing the {@code Principal}
* resource owner (or client)
* @return the {@link AbstractBuilder} for further configuration
*/
public B principal(Authentication principal) {
return put(PRINCIPAL_AUTHENTICATION_KEY, principal);
}
/**
* Sets the {@link ProviderContext provider context}.
*
* @param providerContext the {@link ProviderContext}
* @return the {@link AbstractBuilder} for further configuration
* @since 0.2.3
*/
public B providerContext(ProviderContext providerContext) {
return put(ProviderContext.class, providerContext);
}
/**
* Sets the {@link OAuth2Authorization authorization}.
*
* @param authorization the {@link OAuth2Authorization}
* @return the {@link AbstractBuilder} for further configuration
*/
public B authorization(OAuth2Authorization authorization) {
return put(OAuth2Authorization.class, authorization);
}
/**
* Sets the authorized scope(s).
*
* @param authorizedScopes the authorized scope(s)
* @return the {@link AbstractBuilder} for further configuration
*/
public B authorizedScopes(Set<String> authorizedScopes) {
return put(AUTHORIZATION_SCOPE_AUTHENTICATION_KEY, authorizedScopes);
}
/**
* Sets the {@link OAuth2TokenType token type}.
*
* @param tokenType the {@link OAuth2TokenType}
* @return the {@link AbstractBuilder} for further configuration
*/
public B tokenType(OAuth2TokenType tokenType) {
return put(OAuth2TokenType.class, tokenType);
}
/**
* Sets the {@link AuthorizationGrantType authorization grant type}.
*
* @param authorizationGrantType the {@link AuthorizationGrantType}
* @return the {@link AbstractBuilder} for further configuration
*/
public B authorizationGrantType(AuthorizationGrantType authorizationGrantType) {
return put(AuthorizationGrantType.class, authorizationGrantType);
}
/**
* Sets the {@link Authentication} representing the authorization grant.
*
* @param authorizationGrant the {@link Authentication} representing the authorization grant
* @return the {@link AbstractBuilder} for further configuration
*/
public B authorizationGrant(Authentication authorizationGrant) {
return put(AUTHORIZATION_GRANT_AUTHENTICATION_KEY, authorizationGrant);
}
/**
* Associates an attribute.
*
* @param key the key for the attribute
* @param value the value of the attribute
* @return the {@link AbstractBuilder} for further configuration
*/
public B put(Object key, Object value) {
Assert.notNull(key, "key cannot be null");
Assert.notNull(value, "value cannot be null");
this.context.put(key, value);
return getThis();
}
/**
* A {@code Consumer} of the attributes {@code Map}
* allowing the ability to add, replace, or remove.
*
* @param contextConsumer a {@link Consumer} of the attributes {@code Map}
* @return the {@link AbstractBuilder} for further configuration
*/
public B context(Consumer<Map<Object, Object>> contextConsumer) {
contextConsumer.accept(this.context);
return getThis();
}
@SuppressWarnings("unchecked")
protected <V> V get(Object key) {
return (V) this.context.get(key);
}
protected Map<Object, Object> getContext() {
return this.context;
}
@SuppressWarnings("unchecked")
protected final B getThis() {
return (B) this;
}
/**
* Builds a new {@link OAuth2TokenContext}.
*
* @return the {@link OAuth2TokenContext}
*/
public abstract T build();
}
}

View File

@ -1,223 +0,0 @@
package run.halo.app.identity.authentication;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* @author guqing
* @since 2.0.0
*/
public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for access token requests.
*/
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/api/v1/oauth2/token";
private final AuthenticationManager authenticationManager;
private final RequestMatcher tokenEndpointMatcher;
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
new OAuth2AccessTokenResponseHttpMessageConverter();
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter =
new OAuth2ErrorHttpMessageConverter();
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
new WebAuthenticationDetailsSource();
private AuthenticationConverter authenticationConverter;
private AuthenticationSuccessHandler authenticationSuccessHandler =
this::sendAccessTokenResponse;
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
/**
* Constructs an {@code OAuth2TokenEndpointFilter} using the provided parameters.
*
* @param authenticationManager the authentication manager
*/
public OAuth2TokenEndpointFilter(AuthenticationManager authenticationManager) {
this(authenticationManager, DEFAULT_TOKEN_ENDPOINT_URI);
}
/**
* Constructs an {@code OAuth2TokenEndpointFilter} using the provided parameters.
*
* @param authenticationManager the authentication manager
* @param tokenEndpointUri the endpoint {@code URI} for access token requests
*/
public OAuth2TokenEndpointFilter(AuthenticationManager authenticationManager,
String tokenEndpointUri) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.hasText(tokenEndpointUri, "tokenEndpointUri cannot be empty");
this.authenticationManager = authenticationManager;
this.tokenEndpointMatcher =
new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name());
this.authenticationConverter = new DelegatingAuthenticationConverter(
List.of(new OAuth2RefreshTokenAuthenticationConverter(),
new OAuth2PasswordAuthenticationConverter())
);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
if (!this.tokenEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
String[] grantTypes = request.getParameterValues(OAuth2ParameterNames.GRANT_TYPE);
if (grantTypes == null || grantTypes.length != 1) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.GRANT_TYPE, OAuth2EndpointUtils.ERROR_URI);
}
Authentication authorizationGrantAuthentication =
this.authenticationConverter.convert(request);
if (authorizationGrantAuthentication == null) {
OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE,
OAuth2ParameterNames.GRANT_TYPE, OAuth2EndpointUtils.ERROR_URI);
}
if (authorizationGrantAuthentication instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken) authorizationGrantAuthentication)
.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(
authorizationGrantAuthentication);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response,
accessTokenAuthentication);
} catch (OAuth2AuthenticationException ex) {
SecurityContextHolder.clearContext();
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
}
/**
* Sets the {@link AuthenticationDetailsSource} used for building an authentication details
* instance from {@link HttpServletRequest}.
*
* @param authenticationDetailsSource the {@link AuthenticationDetailsSource} used for
* building an authentication details instance from {@link HttpServletRequest}
*/
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract an Access Token
* Request from {@link HttpServletRequest}
* to an instance of {@link OAuth2AuthorizationGrantAuthenticationToken} used for
* authenticating the authorization grant.
*
* @param authenticationConverter the {@link AuthenticationConverter} used when attempting to
* extract an Access Token Request from {@link HttpServletRequest}
*/
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2AccessTokenAuthenticationToken}
* and returning the {@link OAuth2AccessTokenResponse Access Token Response}.
*
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used for
* handling an {@link OAuth2AccessTokenAuthenticationToken}
*/
public void setAuthenticationSuccessHandler(
AuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException}
* and returning the {@link OAuth2Error Error Response}.
*
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthenticationException}
*/
public void setAuthenticationFailureHandler(
AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
}
private void sendAccessTokenResponse(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) authentication;
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
Map<String, Object> additionalParameters =
accessTokenAuthentication.getAdditionalParameters();
OAuth2AccessTokenResponse.Builder builder =
OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType())
.scopes(accessToken.getScopes());
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
builder.expiresIn(
ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
}
if (refreshToken != null) {
builder.refreshToken(refreshToken.getTokenValue());
}
if (!CollectionUtils.isEmpty(additionalParameters)) {
builder.additionalParameters(additionalParameters);
}
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
}
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.BAD_REQUEST);
this.errorHttpResponseConverter.write(error, null, httpResponse);
}
}

View File

@ -1,35 +0,0 @@
package run.halo.app.identity.authentication;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.OAuth2Token;
/**
* Implementations of this interface are responsible for generating an {@link OAuth2Token}
* using the attributes contained in the {@link OAuth2TokenContext}.
*
* @param <T> the type of the OAuth 2.0 Token
* @author guqing
* @since 2.0.0
* @see OAuth2Token
* @see OAuth2TokenContext
* @see ClaimAccessor
*/
@FunctionalInterface
public interface OAuth2TokenGenerator<T extends OAuth2Token> {
/**
* Generate an OAuth 2.0 Token using the attributes contained in the {@link OAuth2TokenContext},
* or return {@code null} if the {@link OAuth2TokenContext#getTokenType()} is not supported.
*
* <p>
* If the returned {@link OAuth2Token} has a set of claims, it should implement
* {@link ClaimAccessor}
* in order for it to be stored with the {@link OAuth2Authorization}.
*
* @param context the context containing the OAuth 2.0 Token attributes
* @return an {@link OAuth2Token} or {@code null} if the
* {@link OAuth2TokenContext#getTokenType()} is not supported
*/
@Nullable
T generate(OAuth2TokenContext context);
}

View File

@ -1,22 +0,0 @@
package run.halo.app.identity.authentication;
import java.io.Serializable;
import org.springframework.util.Assert;
/**
* @author guqing
* @since 2.0.0
*/
public record OAuth2TokenType(String value) implements Serializable {
public static final OAuth2TokenType ACCESS_TOKEN = new OAuth2TokenType("access_token");
public static final OAuth2TokenType REFRESH_TOKEN = new OAuth2TokenType("refresh_token");
/**
* Constructs an {@code OAuth2TokenType} using the provided value.
*
* @param value the value of the token type
*/
public OAuth2TokenType {
Assert.hasText(value, "value cannot be empty");
}
}

View File

@ -1,49 +0,0 @@
package run.halo.app.identity.authentication;
import java.util.function.Supplier;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A context that holds information of the Provider.
*
* @author guqing
* @since 2.0.0
*/
public record ProviderContext(ProviderSettings providerSettings,
@Nullable Supplier<String> issuerSupplier) {
/**
* Constructs a {@code ProviderContext} using the provided parameters.
*
* @param providerSettings the provider settings
* @param issuerSupplier a {@code Supplier} for the {@code URL} of the Provider's issuer
* identifier
*/
public ProviderContext {
Assert.notNull(providerSettings, "providerSettings cannot be null");
}
/**
* Returns the {@link ProviderSettings}.
*
* @return the {@link ProviderSettings}
*/
@Override
public ProviderSettings providerSettings() {
return this.providerSettings;
}
/**
* Returns the {@code URL} of the Provider's issuer identifier.
* The issuer identifier is resolved from the constructor parameter {@code Supplier<String>}
* or if not provided then defaults to {@link ProviderSettings#getIssuer()}.
*
* @return the {@code URL} of the Provider's issuer identifier
*/
public String getIssuer() {
return this.issuerSupplier != null
? this.issuerSupplier.get() :
providerSettings().getIssuer();
}
}

View File

@ -1,65 +0,0 @@
package run.halo.app.identity.authentication;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.UriComponentsBuilder;
/**
* A {@code Filter} that associates the {@link ProviderContext} to the
* {@link ProviderContextHolder}.
*
* @author guqing
* @see ProviderContext
* @see ProviderContextHolder
* @see ProviderSettings
* @since 2.0.0
*/
public final class ProviderContextFilter extends OncePerRequestFilter {
private final ProviderSettings providerSettings;
/**
* Constructs a {@code ProviderContextFilter} using the provided parameters.
*
* @param providerSettings the provider settings
*/
public ProviderContextFilter(ProviderSettings providerSettings) {
Assert.notNull(providerSettings, "providerSettings cannot be null");
this.providerSettings = providerSettings;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
ProviderContext providerContext = new ProviderContext(
this.providerSettings, () -> resolveIssuer(this.providerSettings, request));
ProviderContextHolder.setProviderContext(providerContext);
filterChain.doFilter(request, response);
} finally {
ProviderContextHolder.resetProviderContext();
}
}
private static String resolveIssuer(ProviderSettings providerSettings,
HttpServletRequest request) {
return providerSettings.getIssuer() != null
? providerSettings.getIssuer() : getContextPath(request);
}
private static String getContextPath(HttpServletRequest request) {
return UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.build()
.toUriString();
}
}

View File

@ -1,47 +0,0 @@
package run.halo.app.identity.authentication;
/**
* A holder of {@link ProviderContext} that associates it with the current thread using a {@code
* ThreadLocal}.
*
* @author guqing
* @see ProviderContext
* @see ProviderContextFilter
* @since 2.0.0
*/
public final class ProviderContextHolder {
private static final ThreadLocal<ProviderContext> holder = new ThreadLocal<>();
private ProviderContextHolder() {
}
/**
* Returns the {@link ProviderContext} bound to the current thread.
*
* @return the {@link ProviderContext}
*/
public static ProviderContext getProviderContext() {
return holder.get();
}
/**
* Bind the given {@link ProviderContext} to the current thread.
*
* @param providerContext the {@link ProviderContext}
*/
public static void setProviderContext(ProviderContext providerContext) {
if (providerContext == null) {
resetProviderContext();
} else {
holder.set(providerContext);
}
}
/**
* Reset the {@link ProviderContext} bound to the current thread.
*/
public static void resetProviderContext() {
holder.remove();
}
}

View File

@ -1,211 +0,0 @@
package run.halo.app.identity.authentication;
import java.time.Duration;
import java.util.Map;
import org.springframework.util.Assert;
import run.halo.app.infra.config.AbstractSettings;
import run.halo.app.infra.config.ConfigurationSettingNames;
/**
* A facility for provider configuration settings.
*
* @author guqing
* @since 2.0.0
*/
public final class ProviderSettings extends AbstractSettings {
private ProviderSettings(Map<String, Object> settings) {
super(settings);
}
/**
* Returns the URL of the Provider's Issuer Identifier.
*
* @return the URL of the Provider's Issuer Identifier
*/
public String getIssuer() {
return getSetting(ConfigurationSettingNames.Provider.ISSUER);
}
/**
* Returns the Provider's OAuth 2.0 Authorization endpoint. The default is {@code /oauth2
* /authorize}.
*
* @return the Authorization endpoint
*/
public String getAuthorizationEndpoint() {
return getSetting(ConfigurationSettingNames.Provider.AUTHORIZATION_ENDPOINT);
}
/**
* Returns the Provider's OAuth 2.0 Token endpoint. The default is {@code /oauth2/token}.
*
* @return the Token endpoint
*/
public String getTokenEndpoint() {
return getSetting(ConfigurationSettingNames.Provider.TOKEN_ENDPOINT);
}
/**
* Returns the Provider's JWK Set endpoint. The default is {@code /oauth2/jwks}.
*
* @return the JWK Set endpoint
*/
public String getJwkSetEndpoint() {
return getSetting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT);
}
/**
* Returns the time-to-live for an access token. The default is 5 minutes.
*
* @return the time-to-live for an access token
*/
public Duration getAccessTokenTimeToLive() {
return getSetting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE);
}
/**
* Returns {@code true} if refresh tokens are reused when returning the access token response,
* or {@code false} if a new refresh token is issued. The default is {@code true}.
*/
public boolean isReuseRefreshTokens() {
return getSetting(ConfigurationSettingNames.Token.REUSE_REFRESH_TOKENS);
}
/**
* Returns the time-to-live for a refresh token. The default is 60 minutes.
*
* @return the time-to-live for a refresh token
*/
public Duration getRefreshTokenTimeToLive() {
return getSetting(ConfigurationSettingNames.Token.REFRESH_TOKEN_TIME_TO_LIVE);
}
/**
* Constructs a new {@link Builder} with the default settings.
*
* @return the {@link Builder}
*/
public static Builder builder() {
return new Builder()
.authorizationEndpoint("/api/v1/oauth2/authorize")
.tokenEndpoint("/api/v1/oauth2/token")
.jwkSetEndpoint("/api/v1/oauth2/jwks")
.accessTokenTimeToLive(Duration.ofMinutes(5L))
.refreshTokenTimeToLive(Duration.ofMinutes(60))
.reuseRefreshTokens(false);
}
/**
* Constructs a new {@link Builder} with the provided settings.
*
* @param settings the settings to initialize the builder
* @return the {@link Builder}
*/
public static Builder withSettings(Map<String, Object> settings) {
Assert.notEmpty(settings, "settings cannot be empty");
return new Builder()
.settings(s -> s.putAll(settings));
}
/**
* A builder for {@link ProviderSettings}.
*/
public static class Builder extends AbstractBuilder<ProviderSettings, Builder> {
private Builder() {
}
/**
* Sets the URL the Provider uses as its Issuer Identifier.
*
* @param issuer the URL the Provider uses as its Issuer Identifier.
* @return the {@link Builder} for further configuration
*/
public Builder issuer(String issuer) {
return setting(ConfigurationSettingNames.Provider.ISSUER, issuer);
}
/**
* Sets the Provider's OAuth 2.0 Authorization endpoint.
*
* @param authorizationEndpoint the Authorization endpoint
* @return the {@link Builder} for further configuration
*/
public Builder authorizationEndpoint(String authorizationEndpoint) {
return setting(ConfigurationSettingNames.Provider.AUTHORIZATION_ENDPOINT,
authorizationEndpoint);
}
/**
* Sets the Provider's OAuth 2.0 Token endpoint.
*
* @param tokenEndpoint the Token endpoint
* @return the {@link Builder} for further configuration
*/
public Builder tokenEndpoint(String tokenEndpoint) {
return setting(ConfigurationSettingNames.Provider.TOKEN_ENDPOINT, tokenEndpoint);
}
/**
* Sets the Provider's JWK Set endpoint.
*
* @param jwkSetEndpoint the JWK Set endpoint
* @return the {@link Builder} for further configuration
*/
public Builder jwkSetEndpoint(String jwkSetEndpoint) {
return setting(ConfigurationSettingNames.Provider.JWK_SET_ENDPOINT, jwkSetEndpoint);
}
/**
* Set the time-to-live for an access token. Must be greater than {@code Duration.ZERO}.
*
* @param accessTokenTimeToLive the time-to-live for an access token
* @return the {@link Builder} for further configuration
*/
public Builder accessTokenTimeToLive(Duration accessTokenTimeToLive) {
Assert.notNull(accessTokenTimeToLive, "accessTokenTimeToLive cannot be null");
Assert.isTrue(accessTokenTimeToLive.getSeconds() > 0,
"accessTokenTimeToLive must be greater than Duration.ZERO");
return setting(ConfigurationSettingNames.Token.ACCESS_TOKEN_TIME_TO_LIVE,
accessTokenTimeToLive);
}
/**
* Set to {@code true} if refresh tokens are reused when returning the access token
* response,
* or {@code false} if a new refresh token is issued.
*
* @param reuseRefreshTokens {@code true} to reuse refresh tokens, {@code false} to issue
* new refresh tokens
* @return the {@link Builder} for further configuration
*/
public Builder reuseRefreshTokens(boolean reuseRefreshTokens) {
return setting(ConfigurationSettingNames.Token.REUSE_REFRESH_TOKENS,
reuseRefreshTokens);
}
/**
* Set the time-to-live for a refresh token. Must be greater than {@code Duration.ZERO}.
*
* @param refreshTokenTimeToLive the time-to-live for a refresh token
* @return the {@link Builder} for further configuration
*/
public Builder refreshTokenTimeToLive(Duration refreshTokenTimeToLive) {
Assert.notNull(refreshTokenTimeToLive, "refreshTokenTimeToLive cannot be null");
Assert.isTrue(refreshTokenTimeToLive.getSeconds() > 0,
"refreshTokenTimeToLive must be greater than Duration.ZERO");
return setting(ConfigurationSettingNames.Token.REFRESH_TOKEN_TIME_TO_LIVE,
refreshTokenTimeToLive);
}
/**
* Builds the {@link ProviderSettings}.
*
* @return the {@link ProviderSettings}
*/
@Override
public ProviderSettings build() {
return new ProviderSettings(getSettings());
}
}
}

View File

@ -1,91 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import java.util.Collection;
import java.util.Map;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
/**
* Base class for {@link AbstractAuthenticationToken} implementations that expose common
* attributes between different OAuth 2.0 Access Token Formats.
*
* <p>For example, a {@link Jwt} could expose its {@link Jwt#getClaims() claims} via
* {@link #getTokenAttributes()} or an &quot;Introspected&quot; OAuth 2.0 Access Token
* could expose the attributes of the Introspection Response via
* {@link #getTokenAttributes()}.
*
* @author guqing
* @see OAuth2AccessToken
* @see Jwt
* @see <a href="https://tools.ietf.org/search/rfc7662#section-2.2">2.2 Introspection Response</a>
* @since 2.0.0
*/
public abstract class AbstractOAuth2TokenAuthenticationToken<T extends AbstractOAuth2Token>
extends AbstractAuthenticationToken {
private Object principal;
private Object credentials;
private T token;
/**
* Sub-class constructor.
*/
protected AbstractOAuth2TokenAuthenticationToken(T token) {
this(token, null);
}
/**
* Sub-class constructor.
*
* @param authorities the authorities assigned to the Access Token
*/
protected AbstractOAuth2TokenAuthenticationToken(T token,
Collection<? extends GrantedAuthority> authorities) {
this(token, token, token, authorities);
}
protected AbstractOAuth2TokenAuthenticationToken(T token, Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
Assert.notNull(token, "token cannot be null");
Assert.notNull(principal, "principal cannot be null");
this.principal = principal;
this.credentials = credentials;
this.token = token;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return this.credentials;
}
/**
* Get the token bound to this {@link Authentication}.
*/
public final T getToken() {
return this.token;
}
/**
* Returns the attributes of the access token.
*
* @return a {@code Map} of the attributes in the access token.
*/
public abstract Map<String, Object> getTokenAttributes();
}

View File

@ -1,47 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.util.Assert;
/**
* An {@link org.springframework.security.core.Authentication} token that represents a
* successful authentication as obtained through a bearer token.
*
* @author guqing
* @since 2.0.0
*/
@Transient
public class BearerTokenAuthentication
extends AbstractOAuth2TokenAuthenticationToken<OAuth2AccessToken> {
private final Map<String, Object> attributes;
/**
* Constructs a {@link BearerTokenAuthentication} with the provided arguments.
*
* @param principal The OAuth 2.0 attributes
* @param credentials The verified token
* @param authorities The authorities associated with the given token
*/
public BearerTokenAuthentication(OAuth2AuthenticatedPrincipal principal,
OAuth2AccessToken credentials,
Collection<? extends GrantedAuthority> authorities) {
super(credentials, principal, credentials, authorities);
Assert.isTrue(credentials.getTokenType() == OAuth2AccessToken.TokenType.BEARER,
"credentials must be a bearer token");
this.attributes =
Collections.unmodifiableMap(new LinkedHashMap<>(principal.getAttributes()));
setAuthenticated(true);
}
@Override
public Map<String, Object> getTokenAttributes() {
return this.attributes;
}
}

View File

@ -1,86 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.util.StringUtils;
/**
* @author guqing
* @since 2.0.0
*/
public class BearerTokenAuthenticationEntryPoint implements AuthenticationEntryPoint {
private String realmName;
/**
* Collect error details from the provided parameters and format according to RFC
* 6750, specifically {@code error}, {@code error_description}, {@code error_uri}, and
* {@code scope}.
*
* @param request that resulted in an <code>AuthenticationException</code>
* @param response so that the user agent can begin authentication
* @param authException that caused the invocation
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) {
HttpStatus status = HttpStatus.UNAUTHORIZED;
Map<String, String> parameters = new LinkedHashMap<>();
if (this.realmName != null) {
parameters.put("realm", this.realmName);
}
if (authException instanceof OAuth2AuthenticationException) {
OAuth2Error error = ((OAuth2AuthenticationException) authException).getError();
parameters.put("error", error.getErrorCode());
if (StringUtils.hasText(error.getDescription())) {
parameters.put("error_description", error.getDescription());
}
if (StringUtils.hasText(error.getUri())) {
parameters.put("error_uri", error.getUri());
}
if (error instanceof BearerTokenError bearerTokenError) {
if (StringUtils.hasText(bearerTokenError.getScope())) {
parameters.put("scope", bearerTokenError.getScope());
}
status = ((BearerTokenError) error).getHttpStatus();
}
}
String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters);
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
response.setStatus(status.value());
}
/**
* Set the default realm name to use in the bearer token error response.
*
* @param realmName realm name
*/
public void setRealmName(String realmName) {
this.realmName = realmName;
}
private static String computeWWWAuthenticateHeaderValue(Map<String, String> parameters) {
StringBuilder wwwAuthenticate = new StringBuilder();
wwwAuthenticate.append("Bearer");
if (!parameters.isEmpty()) {
wwwAuthenticate.append(" ");
int i = 0;
for (Map.Entry<String, String> entry : parameters.entrySet()) {
wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue())
.append("\"");
if (i != parameters.size() - 1) {
wwwAuthenticate.append(", ");
}
i++;
}
}
return wwwAuthenticate.toString();
}
}

View File

@ -1,196 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.core.log.LogMessage;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.context.NullSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* <p>Authenticates requests that contain an OAuth 2.0
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>.</p>
* This filter should be wired with an {@link AuthenticationManager} that can authenticate
* a {@link BearerTokenAuthenticationToken}.
*
* @author guqing
* @see
* <a href="https://tools.ietf.org/html/rfc6750">The OAuth 2.0 Authorization Framework: Bearer Token Usage</a>
* @see JwtAuthenticationProvider
* @since 2.0.0
*/
public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
private final AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver;
private AuthenticationEntryPoint authenticationEntryPoint =
new BearerTokenAuthenticationEntryPoint();
private AuthenticationFailureHandler authenticationFailureHandler =
(request, response, exception) -> {
if (exception instanceof AuthenticationServiceException) {
throw exception;
}
this.authenticationEntryPoint.commence(request, response, exception);
};
private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource =
new WebAuthenticationDetailsSource();
private SecurityContextRepository securityContextRepository =
new NullSecurityContextRepository();
/**
* Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s).
*
* @param authenticationManagerResolver authentication manager resolver
*/
public BearerTokenAuthenticationFilter(
AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver) {
Assert.notNull(authenticationManagerResolver,
"authenticationManagerResolver cannot be null");
this.authenticationManagerResolver = authenticationManagerResolver;
}
/**
* Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s).
*
* @param authenticationManager authentication manager
*/
public BearerTokenAuthenticationFilter(AuthenticationManager authenticationManager) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
this.authenticationManagerResolver = (request) -> authenticationManager;
}
/**
* Extract any
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>
* from the request and attempt an authentication.
*
* @param request http servlet request
* @param response http servlet response
* @param filterChain filter chain
*/
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
throws ServletException, IOException {
String token;
try {
token = this.bearerTokenResolver.resolve(request);
} catch (OAuth2AuthenticationException invalid) {
this.logger.trace(
"Sending to authentication entry point since failed to resolve bearer token",
invalid);
this.authenticationEntryPoint.commence(request, response, invalid);
return;
}
if (token == null) {
this.logger.trace("Did not process request since did not find bearer token");
filterChain.doFilter(request, response);
return;
}
BearerTokenAuthenticationToken authenticationRequest =
new BearerTokenAuthenticationToken(token);
authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
try {
AuthenticationManager authenticationManager =
this.authenticationManagerResolver.resolve(request);
Authentication authenticationResult =
authenticationManager.authenticate(authenticationRequest);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authenticationResult);
SecurityContextHolder.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
this.logger.debug(
LogMessage.format("Set SecurityContextHolder to %s", authenticationResult));
filterChain.doFilter(request, response);
} catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
}
}
/**
* Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
* authentication success. The default action is not to save the
* {@link SecurityContext}.
*
* @param securityContextRepository the {@link SecurityContextRepository} to use.
* Cannot be null.
*/
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
this.securityContextRepository = securityContextRepository;
}
/**
* Set the {@link BearerTokenResolver} to use. Defaults to
* {@link DefaultBearerTokenResolver}.
*
* @param bearerTokenResolver the {@code BearerTokenResolver} to use
*/
public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) {
Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null");
this.bearerTokenResolver = bearerTokenResolver;
}
/**
* Set the {@link AuthenticationEntryPoint} to use. Defaults to
* {@link BearerTokenAuthenticationEntryPoint}.
*
* @param authenticationEntryPoint the {@code AuthenticationEntryPoint} to use
*/
public void setAuthenticationEntryPoint(
final AuthenticationEntryPoint authenticationEntryPoint) {
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
this.authenticationEntryPoint = authenticationEntryPoint;
}
/**
* Set the {@link AuthenticationFailureHandler} to use. Default implementation invokes
* {@link AuthenticationEntryPoint}.
*
* @param authenticationFailureHandler the {@code AuthenticationFailureHandler} to use
* @since 5.2
*/
public void setAuthenticationFailureHandler(
final AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
}
/**
* Set the {@link AuthenticationDetailsSource} to use. Defaults to
* {@link WebAuthenticationDetailsSource}.
*
* @param authenticationDetailsSource the {@code AuthenticationConverter} to use
* @since 5.5
*/
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
}
}

View File

@ -1,49 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import java.util.Collections;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* An {@link Authentication} that contains a
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>.
*
* @author guqing
* @since 2.0.0
*/
public class BearerTokenAuthenticationToken extends AbstractAuthenticationToken {
private final String token;
/**
* Create a {@code BearerTokenAuthenticationToken} using the provided parameter(s).
*
* @param token - the bearer token
*/
public BearerTokenAuthenticationToken(String token) {
super(Collections.emptyList());
Assert.hasText(token, "token cannot be empty");
this.token = token;
}
/**
* Get the
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>.
*
* @return the token that proves the caller's authority to perform the
* {@link jakarta.servlet.http.HttpServletRequest}
*/
public String getToken() {
return this.token;
}
@Override
public Object getCredentials() {
return this.getToken();
}
@Override
public Object getPrincipal() {
return this.getToken();
}
}

View File

@ -1,110 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import org.springframework.http.HttpStatus;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.util.Assert;
/**
* A representation of a
* <a href="https://tools.ietf.org/html/rfc6750#section-3.1">Bearer Token Error</a>.
*
* @author guqing
* @see BearerTokenErrorCodes
* @see
* <a href="https://tools.ietf.org/html/rfc6750#section-3">RFC 6750 Section 3: The WWW-Authenticate Response Header Field</a>
* @see
* <a href="https://github.com/spring-projects/spring-security/blob/e79b6b3ac8/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/BearerTokenError.java">oauth2 resource server BearerTokenError</a>
* @since 2.0.0
*/
public final class BearerTokenError extends OAuth2Error {
private final HttpStatus httpStatus;
private final String scope;
/**
* Create a {@code BearerTokenError} using the provided parameters.
*
* @param errorCode the error code
* @param httpStatus the HTTP status
*/
public BearerTokenError(String errorCode, HttpStatus httpStatus, String description,
String errorUri) {
this(errorCode, httpStatus, description, errorUri, null);
}
/**
* Create a {@code BearerTokenError} using the provided parameters.
*
* @param errorCode the error code
* @param httpStatus the HTTP status
* @param description the description
* @param errorUri the URI
* @param scope the scope
*/
public BearerTokenError(String errorCode, HttpStatus httpStatus, String description,
String errorUri,
String scope) {
super(errorCode, description, errorUri);
Assert.notNull(httpStatus, "httpStatus cannot be null");
Assert.isTrue(isDescriptionValid(description),
"description contains invalid ASCII characters, it must conform to RFC 6750");
Assert.isTrue(isErrorCodeValid(errorCode),
"errorCode contains invalid ASCII characters, it must conform to RFC 6750");
Assert.isTrue(isErrorUriValid(errorUri),
"errorUri contains invalid ASCII characters, it must conform to RFC 6750");
Assert.isTrue(isScopeValid(scope),
"scope contains invalid ASCII characters, it must conform to RFC 6750");
this.httpStatus = httpStatus;
this.scope = scope;
}
/**
* Return the HTTP status.
*
* @return the HTTP status
*/
public HttpStatus getHttpStatus() {
return this.httpStatus;
}
/**
* Return the scope.
*
* @return the scope
*/
public String getScope() {
return this.scope;
}
private static boolean isDescriptionValid(String description) {
return description == null || description.chars().allMatch((c) ->
withinTheRangeOf(c, 0x20, 0x21)
|| withinTheRangeOf(c, 0x23, 0x5B)
|| withinTheRangeOf(c, 0x5D, 0x7E));
}
private static boolean isErrorCodeValid(String errorCode) {
return errorCode.chars().allMatch((c) ->
withinTheRangeOf(c, 0x20, 0x21)
|| withinTheRangeOf(c, 0x23, 0x5B)
|| withinTheRangeOf(c, 0x5D, 0x7E));
}
private static boolean isErrorUriValid(String errorUri) {
return errorUri == null || errorUri.chars()
.allMatch(
(c) -> c == 0x21 || withinTheRangeOf(c, 0x23, 0x5B) || withinTheRangeOf(c, 0x5D,
0x7E));
}
private static boolean isScopeValid(String scope) {
return scope == null || scope.chars().allMatch((c) ->
withinTheRangeOf(c, 0x20, 0x21)
|| withinTheRangeOf(c, 0x23, 0x5B)
|| withinTheRangeOf(c, 0x5D, 0x7E));
}
private static boolean withinTheRangeOf(int c, int min, int max) {
return c >= min && c <= max;
}
}

View File

@ -1,31 +0,0 @@
package run.halo.app.identity.authentication.verifier;
/**
* Standard error codes defined by the OAuth 2.0 Authorization Framework: Bearer Token
* Usage.
*
* @author guqing
* @see
* <a href="https://tools.ietf.org/html/rfc6750#section-3.1">RFC 6750 Section 3.1: Error Codes</a>
* @since 2.0.0
*/
public interface BearerTokenErrorCodes {
/**
* {@code invalid_request} - The request is missing a required parameter, includes an
* unsupported parameter or parameter value, repeats the same parameter, uses more
* than one method for including an access token, or is otherwise malformed.
*/
String INVALID_REQUEST = "invalid_request";
/**
* {@code invalid_token} - The access token provided is expired, revoked, malformed,
* or invalid for other reasons.
*/
String INVALID_TOKEN = "invalid_token";
/**
* {@code insufficient_scope} - The request requires higher privileges than provided
* by the access token.
*/
String INSUFFICIENT_SCOPE = "insufficient_scope";
}

View File

@ -1,79 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import org.springframework.http.HttpStatus;
/**
* A factory for creating {@link BearerTokenError} instances that correspond to the
* registered <a href="https://tools.ietf.org/html/rfc6750#section-3.1">Bearer Token Error Codes</a>.
*
* @author guqing
* @since 2.0.0
*/
public class BearerTokenErrors {
private static final BearerTokenError DEFAULT_INVALID_REQUEST =
invalidRequest("Invalid request");
private static final BearerTokenError DEFAULT_INVALID_TOKEN = invalidToken("Invalid token");
private static final BearerTokenError DEFAULT_INSUFFICIENT_SCOPE =
insufficientScope("Insufficient scope", null);
private static final String DEFAULT_URI = "https://tools.ietf.org/html/rfc6750#section-3.1";
private BearerTokenErrors() {
}
/**
* Create a {@link BearerTokenError} caused by an invalid request.
*
* @param message a description of the error
* @return a {@link BearerTokenError}
*/
public static BearerTokenError invalidRequest(String message) {
try {
return new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST,
HttpStatus.BAD_REQUEST, message,
DEFAULT_URI);
} catch (IllegalArgumentException ex) {
// some third-party library error messages are not suitable for RFC 6750's
// error message charset
return DEFAULT_INVALID_REQUEST;
}
}
/**
* Create a {@link BearerTokenError} caused by an invalid token.
*
* @param message a description of the error
* @return a {@link BearerTokenError}
*/
public static BearerTokenError invalidToken(String message) {
try {
return new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN,
HttpStatus.UNAUTHORIZED, message,
DEFAULT_URI);
} catch (IllegalArgumentException ex) {
// some third-party library error messages are not suitable for RFC 6750's
// error message charset
return DEFAULT_INVALID_TOKEN;
}
}
/**
* Create a {@link BearerTokenError} caused by an invalid token.
*
* @param scope the scope attribute to use in the error
* @return a {@link BearerTokenError}
*/
public static BearerTokenError insufficientScope(String message, String scope) {
try {
return new BearerTokenError(BearerTokenErrorCodes.INSUFFICIENT_SCOPE,
HttpStatus.FORBIDDEN, message,
DEFAULT_URI, scope);
} catch (IllegalArgumentException ex) {
// some third-party library error messages are not suitable for RFC 6750's
// error message charset
return DEFAULT_INSUFFICIENT_SCOPE;
}
}
}

View File

@ -1,29 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
/**
* A strategy for resolving
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>
* s from the {@link HttpServletRequest}.
*
* @author guqing
* @see
* <a href="https://tools.ietf.org/html/rfc6750#section-2">RFC 6750 Section 2: Authenticated Requests</a>
* @since 2.0.0
*/
@FunctionalInterface
public interface BearerTokenResolver {
/**
* Resolve any
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>
* value from the request.
*
* @param request the request
* @return the Bearer Token value or {@code null} if none found
* @throws OAuth2AuthenticationException if the found token is invalid
*/
String resolve(HttpServletRequest request);
}

View File

@ -1,126 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import jakarta.servlet.http.HttpServletRequest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.util.StringUtils;
/**
* The default {@link BearerTokenResolver} implementation based on RFC 6750.
*
* @author guqing
* @see
* <a href="https://tools.ietf.org/html/rfc6750#section-2">RFC 6750 Section 2: Authenticated Requests</a>
* @since 2.0.0
*/
public final class DefaultBearerTokenResolver implements BearerTokenResolver {
private static final Pattern authorizationPattern =
Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$",
Pattern.CASE_INSENSITIVE);
private boolean allowFormEncodedBodyParameter = false;
private boolean allowUriQueryParameter = false;
private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
@Override
public String resolve(final HttpServletRequest request) {
final String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
final String parameterToken = isParameterTokenSupportedForRequest(request)
? resolveFromRequestParameters(request) : null;
if (authorizationHeaderToken != null) {
if (parameterToken != null) {
final BearerTokenError error = BearerTokenErrors
.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
return authorizationHeaderToken;
}
if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
return parameterToken;
}
return null;
}
/**
* Set if transport of access token using form-encoded body parameter is supported.
* Defaults to {@code false}.
*
* @param allowFormEncodedBodyParameter if the form-encoded body parameter is
* supported
*/
public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) {
this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter;
}
/**
* Set if transport of access token using URI query parameter is supported. Defaults
* to {@code false}.
* <p>
* The spec recommends against using this mechanism for sending bearer tokens, and
* even goes as far as stating that it was only included for completeness.
*
* @param allowUriQueryParameter if the URI query parameter is supported
*/
public void setAllowUriQueryParameter(boolean allowUriQueryParameter) {
this.allowUriQueryParameter = allowUriQueryParameter;
}
/**
* Set this value to configure what header is checked when resolving a Bearer Token.
* This value is defaulted to {@link HttpHeaders#AUTHORIZATION}.
* <p>
* This allows other headers to be used as the Bearer Token source such as
* {@link HttpHeaders#PROXY_AUTHORIZATION}
*
* @param bearerTokenHeaderName the header to check when retrieving the Bearer Token.
* @since 5.4
*/
public void setBearerTokenHeaderName(String bearerTokenHeaderName) {
this.bearerTokenHeaderName = bearerTokenHeaderName;
}
private String resolveFromAuthorizationHeader(HttpServletRequest request) {
String authorization = request.getHeader(this.bearerTokenHeaderName);
if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
return null;
}
Matcher matcher = authorizationPattern.matcher(authorization);
if (!matcher.matches()) {
BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
throw new OAuth2AuthenticationException(error);
}
return matcher.group("token");
}
private static String resolveFromRequestParameters(HttpServletRequest request) {
String[] values = request.getParameterValues("access_token");
if (values == null || values.length == 0) {
return null;
}
if (values.length == 1) {
return values[0];
}
BearerTokenError error =
BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
throw new OAuth2AuthenticationException(error);
}
private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) {
return (("POST".equals(request.getMethod())
&& MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
|| "GET".equals(request.getMethod()));
}
private boolean isParameterTokenEnabledForRequest(final HttpServletRequest request) {
return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod())
&& MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
|| (this.allowUriQueryParameter && "GET".equals(request.getMethod())));
}
}

View File

@ -1,40 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
/**
* An {@link OAuth2AuthenticationException} that indicates an invalid bearer token.
*
* @author guqing
* @since 2.0.0
*/
public class InvalidBearerTokenException extends OAuth2AuthenticationException {
/**
* Construct an instance of {@link InvalidBearerTokenException} given the provided
* description.
* <p>
* The description will be wrapped into an
* {@link org.springframework.security.oauth2.core.OAuth2Error} instance as the
* {@code error_description}.
*
* @param description the description
*/
public InvalidBearerTokenException(String description) {
super(BearerTokenErrors.invalidToken(description));
}
/**
* Construct an instance of {@link InvalidBearerTokenException} given the provided
* description and cause
* <p>
* The description will be wrapped into an
* {@link org.springframework.security.oauth2.core.OAuth2Error} instance as the
* {@code error_description}.
*
* @param description the description
* @param cause the causing exception
*/
public InvalidBearerTokenException(String description, Throwable cause) {
super(BearerTokenErrors.invalidToken(description), cause);
}
}

View File

@ -1,57 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
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.security.oauth2.jwt.Jwt;
import run.halo.app.identity.authentication.OAuth2Authorization;
import run.halo.app.identity.authentication.OAuth2AuthorizationService;
import run.halo.app.identity.authentication.OAuth2TokenType;
/**
* <p>An implementation of {@link OAuth2TokenValidator} for verifying a Jwt-based access token has
* none blocked.</p>
* <p>Because the persistent access token will be manually removed or logged out,
* this token should not continue to be used in this case.</p>
*
* @author guqing
* @see OAuth2TokenValidator
* @see OAuth2AuthorizationService
* @since 2.0.0
*/
public class JwtAccessTokenNonBlockedValidator implements OAuth2TokenValidator<Jwt> {
private final OAuth2AuthorizationService oauth2AuthorizationService;
private final OAuth2Error error;
public JwtAccessTokenNonBlockedValidator(
OAuth2AuthorizationService oauth2AuthorizationService) {
this.oauth2AuthorizationService = oauth2AuthorizationService;
this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
"The access token is not valid",
"https://tools.ietf.org/html/rfc6750#section-3.1");
}
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
String tokenValue = token.getTokenValue();
if (tokenValue == null) {
return OAuth2TokenValidatorResult.failure(this.error);
}
OAuth2Authorization oauth2Authorization =
oauth2AuthorizationService.findByToken(tokenValue, OAuth2TokenType.ACCESS_TOKEN);
if (oauth2Authorization == null) {
return OAuth2TokenValidatorResult.failure(this.error);
}
OAuth2Authorization.Token<OAuth2AccessToken> accessToken =
oauth2Authorization.getAccessToken();
if (accessToken == null || accessToken.isExpired()) {
return OAuth2TokenValidatorResult.failure(this.error);
}
return OAuth2TokenValidatorResult.success();
}
}

View File

@ -1,55 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import java.util.Collection;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.util.Assert;
/**
* @author guqing
* @since 2.0.0
*/
public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter =
new JwtGrantedAuthoritiesConverter();
private String principalClaimName = JwtClaimNames.SUB;
@Override
public final AbstractAuthenticationToken convert(@NonNull Jwt jwt) {
Collection<GrantedAuthority> authorities = this.jwtGrantedAuthoritiesConverter.convert(jwt);
String principalClaimValue = jwt.getClaimAsString(this.principalClaimName);
return new JwtAuthenticationToken(jwt, authorities, principalClaimValue);
}
/**
* Sets the {@link Converter Converter&lt;Jwt, Collection&lt;GrantedAuthority&gt;&gt;}
* to use. Defaults to {@link JwtGrantedAuthoritiesConverter}.
*
* @param jwtGrantedAuthoritiesConverter The converter
* @see JwtGrantedAuthoritiesConverter
* @since 5.2
*/
public void setJwtGrantedAuthoritiesConverter(
Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter) {
Assert.notNull(jwtGrantedAuthoritiesConverter,
"jwtGrantedAuthoritiesConverter cannot be null");
this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter;
}
/**
* Sets the principal claim name. Defaults to {@link JwtClaimNames#SUB}.
*
* @param principalClaimName The principal claim name
* @since 5.4
*/
public void setPrincipalClaimName(String principalClaimName) {
Assert.hasText(principalClaimName, "principalClaimName cannot be empty");
this.principalClaimName = principalClaimName;
}
}

View File

@ -1,96 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import java.util.Collection;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.BadJwtException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} implementation of the {@link Jwt}-encoded
* <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 a
* {@link Jwt}-encoded access token, returning its claims set as part of the
* {@link Authentication} statement.
* <p>
* <p>
* Scopes are translated into {@link GrantedAuthority}s according to the following
* algorithm:
* <p>
* 1. If there is a "scope" or "scp" attribute, then if a {@link String}, then split by
* spaces and return, or if a {@link Collection}, then simply return 2. Take the resulting
* {@link Collection} of {@link String}s and prepend the "SCOPE_" keyword, adding as
* {@link GrantedAuthority}s.
*
* @author guqing
* @see AuthenticationProvider
* @see JwtDecoder
* @since 2.0.0
*/
@Slf4j
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final JwtDecoder jwtDecoder;
private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter =
new JwtAuthenticationConverter();
public JwtAuthenticationProvider(JwtDecoder jwtDecoder) {
Assert.notNull(jwtDecoder, "jwtDecoder cannot be null");
this.jwtDecoder = jwtDecoder;
}
/**
* Decode and validate the
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2">Bearer Token</a>.
*
* @param authentication the authentication request object.
* @return A successful authentication
* @throws AuthenticationException if authentication failed for some reason
*/
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
Jwt jwt = getJwt(bearer);
AbstractAuthenticationToken token = this.jwtAuthenticationConverter.convert(jwt);
if (token != null) {
token.setDetails(bearer.getDetails());
}
log.debug("Authenticated token");
return token;
}
private Jwt getJwt(BearerTokenAuthenticationToken bearer) {
try {
return this.jwtDecoder.decode(bearer.getToken());
} catch (BadJwtException failed) {
log.debug("Failed to authenticate since the JWT was invalid");
throw new InvalidBearerTokenException(failed.getMessage(), failed);
} catch (JwtException failed) {
throw new AuthenticationServiceException(failed.getMessage(), failed);
}
}
@Override
public boolean supports(Class<?> authentication) {
return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
}
public void setJwtAuthenticationConverter(
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter) {
Assert.notNull(jwtAuthenticationConverter, "jwtAuthenticationConverter cannot be null");
this.jwtAuthenticationConverter = jwtAuthenticationConverter;
}
}

View File

@ -1,70 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import java.util.Collection;
import java.util.Map;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* An implementation of an {@link AbstractOAuth2TokenAuthenticationToken} representing a
* {@link Jwt} {@code Authentication}.
*
* @author guqing
* @see AbstractOAuth2TokenAuthenticationToken
* @see Jwt
* @since 2.0.0
*/
@Transient
public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken<Jwt> {
private final String name;
/**
* Constructs a {@code JwtAuthenticationToken} using the provided parameters.
*
* @param jwt the JWT
*/
public JwtAuthenticationToken(Jwt jwt) {
super(jwt);
this.name = jwt.getSubject();
}
/**
* Constructs a {@code JwtAuthenticationToken} using the provided parameters.
*
* @param jwt the JWT
* @param authorities the authorities assigned to the JWT
*/
public JwtAuthenticationToken(Jwt jwt, Collection<? extends GrantedAuthority> authorities) {
super(jwt, authorities);
this.setAuthenticated(true);
this.name = jwt.getSubject();
}
/**
* Constructs a {@code JwtAuthenticationToken} using the provided parameters.
*
* @param jwt the JWT
* @param authorities the authorities assigned to the JWT
* @param name the principal name
*/
public JwtAuthenticationToken(Jwt jwt, Collection<? extends GrantedAuthority> authorities,
String name) {
super(jwt, authorities);
this.setAuthenticated(true);
this.name = name;
}
@Override
public Map<String, Object> getTokenAttributes() {
return this.getToken().getClaims();
}
/**
* The principal name which is, by default, the {@link Jwt}'s subject.
*/
@Override
public String getName() {
return this.name;
}
}

View File

@ -1,115 +0,0 @@
package run.halo.app.identity.authentication.verifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Extracts the {@link GrantedAuthority}s from scope attributes typically found in a
* {@link Jwt}.
*
* @author guqing
* @since 2.0.0
*/
@Slf4j
public class JwtGrantedAuthoritiesConverter implements
Converter<Jwt, Collection<GrantedAuthority>> {
private static final String DEFAULT_AUTHORITY_PREFIX = "SCOPE_";
private static final Collection<String> WELL_KNOWN_AUTHORITIES_CLAIM_NAMES =
List.of("scope", "scp");
private String authorityPrefix = DEFAULT_AUTHORITY_PREFIX;
private String authoritiesClaimName;
/**
* Extract {@link GrantedAuthority}s from the given {@link Jwt}.
*
* @param jwt The {@link Jwt} token
* @return The {@link GrantedAuthority authorities} read from the token scopes
*/
@Override
public Collection<GrantedAuthority> convert(@NonNull Jwt jwt) {
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (String authority : getAuthorities(jwt)) {
grantedAuthorities.add(new SimpleGrantedAuthority(this.authorityPrefix + authority));
}
return grantedAuthorities;
}
/**
* Sets the prefix to use for {@link GrantedAuthority authorities} mapped by this
* converter. Defaults to
* {@link JwtGrantedAuthoritiesConverter#DEFAULT_AUTHORITY_PREFIX}.
*
* @param authorityPrefix The authority prefix
* @since 5.2
*/
public void setAuthorityPrefix(String authorityPrefix) {
Assert.notNull(authorityPrefix, "authorityPrefix cannot be null");
this.authorityPrefix = authorityPrefix;
}
/**
* Sets the name of token claim to use for mapping {@link GrantedAuthority
* authorities} by this converter. Defaults to
* {@link JwtGrantedAuthoritiesConverter#WELL_KNOWN_AUTHORITIES_CLAIM_NAMES}.
*
* @param authoritiesClaimName The token claim name to map authorities
* @since 5.2
*/
public void setAuthoritiesClaimName(String authoritiesClaimName) {
Assert.hasText(authoritiesClaimName, "authoritiesClaimName cannot be empty");
this.authoritiesClaimName = authoritiesClaimName;
}
private String getAuthoritiesClaimName(Jwt jwt) {
if (this.authoritiesClaimName != null) {
return this.authoritiesClaimName;
}
for (String claimName : WELL_KNOWN_AUTHORITIES_CLAIM_NAMES) {
if (jwt.hasClaim(claimName)) {
return claimName;
}
}
return null;
}
private Collection<String> getAuthorities(Jwt jwt) {
String claimName = getAuthoritiesClaimName(jwt);
if (claimName == null) {
log.trace(
"Returning no authorities since could not find any claims that might contain "
+ "scopes");
return Collections.emptyList();
}
log.trace("Looking for scopes in claim [{}]", claimName);
Object authorities = jwt.getClaim(claimName);
if (authorities instanceof String) {
if (StringUtils.hasText((String) authorities)) {
return Arrays.asList(((String) authorities).split(" "));
}
return Collections.emptyList();
}
if (authorities instanceof Collection) {
return castAuthoritiesToCollection(authorities);
}
return Collections.emptyList();
}
@SuppressWarnings("unchecked")
private Collection<String> castAuthoritiesToCollection(Object authorities) {
return (Collection<String>) authorities;
}
}

View File

@ -1,58 +0,0 @@
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;
}
}
}

View File

@ -1,13 +0,0 @@
package run.halo.app.identity.authorization;
import java.util.Set;
/**
* @author guqing
* @since 2.0.0
*/
@FunctionalInterface
public interface RoleBindingLister {
Set<String> listBoundRoleNames();
}

View File

@ -1,57 +0,0 @@
package run.halo.app.identity.entrypoint;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.util.Assert;
import run.halo.app.infra.utils.HaloUtils;
/**
* @author guqing
* @date 2022-04-12
*/
@Slf4j
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private String errorPage;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
if (response.isCommitted()) {
log.trace("Did not write to response since already committed");
return;
}
if (this.errorPage == null) {
log.debug("Responding with 403 status code");
response.sendError(HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase());
return;
}
// Put exception into request scope (perhaps of use to a view)
request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
// Set the 403 status code.
response.setStatus(HttpStatus.FORBIDDEN.value());
// forward to error page.
if (log.isDebugEnabled()) {
log.debug("Forwarding to [{}] with status code 403", this.errorPage);
}
if (HaloUtils.isAjaxRequest(request)) {
response.getWriter().write("access denied!");
return;
}
request.getRequestDispatcher(this.errorPage).forward(request, response);
}
public void setErrorPage(String errorPage) {
Assert.isTrue(errorPage == null || errorPage.startsWith("/"),
"errorPage must begin with '/'");
this.errorPage = errorPage;
}
}

View File

@ -1,31 +0,0 @@
package run.halo.app.identity.entrypoint;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import run.halo.app.infra.utils.HaloUtils;
/**
* @author guqing
* @date 2022-04-12
*/
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.warn("Unauthorized error: {}", authException.getMessage());
response.setStatus(HttpStatus.UNAUTHORIZED.value());
if (HaloUtils.isAjaxRequest(request)) {
response.getWriter().write(HttpStatus.UNAUTHORIZED.getReasonPhrase());
return;
}
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
}

View File

@ -1,44 +0,0 @@
package run.halo.app.identity.entrypoint;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import run.halo.app.identity.authentication.OAuth2AuthorizationService;
import run.halo.app.identity.authentication.OAuth2TokenType;
import run.halo.app.identity.authentication.verifier.JwtAuthenticationToken;
/**
* <p>Performs a logout by {@link OAuth2AuthorizationService}.</p>
* <p>Will remove the {@link Authentication} from the {@link OAuth2AuthorizationService} if the
* specific instance of {@link Authentication} is {@link JwtAuthenticationToken}.</p>
*
* @author guqing
* @see OAuth2AuthorizationService
* @see Authentication
* @see JwtAuthenticationToken
* @since 2.0.0
*/
@Slf4j
public class Oauth2LogoutHandler implements LogoutHandler {
private final OAuth2AuthorizationService oauth2AuthorizationService;
public Oauth2LogoutHandler(OAuth2AuthorizationService oauth2AuthorizationService) {
this.oauth2AuthorizationService = oauth2AuthorizationService;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
log.debug("Logging out [{}]", authentication);
if (authentication instanceof JwtAuthenticationToken jwtAuthenticationToken) {
String tokenValue = jwtAuthenticationToken.getToken().getTokenValue();
var oauth2Authorization = oauth2AuthorizationService.findByToken(
tokenValue, OAuth2TokenType.ACCESS_TOKEN);
oauth2AuthorizationService.remove(oauth2Authorization);
log.debug("Removed oauth2Authorization [{}]", oauth2Authorization);
}
}
}

View File

@ -1,106 +1,18 @@
package run.halo.app.infra;
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Set;
import javax.crypto.SecretKey;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.stereotype.Component;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Schemes;
import run.halo.app.extension.Unstructured;
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.Role;
import run.halo.app.infra.utils.HaloUtils;
import run.halo.app.infra.utils.YamlUnstructuredLoader;
import run.halo.app.security.authentication.pat.PersonalAccessToken;
import run.halo.app.security.authorization.Role;
/**
* @author guqing
* @since 2.0.0
*/
@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 The read location of the configuration file needs to be considered later
createUnstructured();
// TODO These test only methods will be removed in the future
initPersonalAccessTokenForTesting();
}
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("role-template-view-categories", "role-template-view-nonresources");
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);
}
private void createUnstructured() {
try {
List<Unstructured> unstructuredList = loadClassPathResourcesToUnstructured();
for (Unstructured unstructured : unstructuredList) {
extensionClient.create(unstructured);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private List<Unstructured> loadClassPathResourcesToUnstructured() throws IOException {
PathMatchingResourcePatternResolver resourcePatternResolver =
new PathMatchingResourcePatternResolver();
// Gets yaml resources
Resource[] yamlResources =
resourcePatternResolver.getResources("classpath*:extensions/*.yaml");
Resource[] ymlResources =
resourcePatternResolver.getResources("classpath*:extensions/*.yml");
YamlUnstructuredLoader yamlUnstructuredLoader =
new YamlUnstructuredLoader(ArrayUtils.addAll(ymlResources, yamlResources));
return yamlUnstructuredLoader.load();
Schemes.INSTANCE.register(PersonalAccessToken.class);
}
}

View File

@ -1,116 +0,0 @@
package run.halo.app.infra.config;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import org.springframework.util.Assert;
/**
* Base implementation for configuration settings.
*
* @author guqing
* @since 2.0.0
*/
public abstract class AbstractSettings implements Serializable {
private final Map<String, Object> settings;
protected AbstractSettings(Map<String, Object> settings) {
Assert.notEmpty(settings, "settings cannot be empty");
this.settings = Collections.unmodifiableMap(new HashMap<>(settings));
}
/**
* Returns a configuration setting.
*
* @param name the name of the setting
* @param <T> the type of the setting
* @return the value of the setting, or {@code null} if not available
*/
@SuppressWarnings("unchecked")
public <T> T getSetting(String name) {
Assert.hasText(name, "name cannot be empty");
return (T) getSettings().get(name);
}
/**
* Returns a {@code Map} of the configuration settings.
*
* @return a {@code Map} of the configuration settings
*/
public Map<String, Object> getSettings() {
return this.settings;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
AbstractSettings that = (AbstractSettings) obj;
return this.settings.equals(that.settings);
}
@Override
public int hashCode() {
return Objects.hash(this.settings);
}
@Override
public String toString() {
return "AbstractSettings {" + "settings=" + this.settings + '}';
}
/**
* A builder for subclasses of {@link AbstractSettings}.
*/
protected abstract static class AbstractBuilder<T extends AbstractSettings,
B extends AbstractBuilder<T, B>> {
private final Map<String, Object> settings = new HashMap<>();
protected AbstractBuilder() {
}
/**
* Sets a configuration setting.
*
* @param name the name of the setting
* @param value the value of the setting
* @return the {@link AbstractBuilder} for further configuration
*/
public B setting(String name, Object value) {
Assert.hasText(name, "name cannot be empty");
Assert.notNull(value, "value cannot be null");
getSettings().put(name, value);
return getThis();
}
/**
* A {@code Consumer} of the configuration settings {@code Map}
* allowing the ability to add, replace, or remove.
*
* @param settingsConsumer a {@link Consumer} of the configuration settings {@code Map}
* @return the {@link AbstractBuilder} for further configuration
*/
public B settings(Consumer<Map<String, Object>> settingsConsumer) {
settingsConsumer.accept(getSettings());
return getThis();
}
public abstract T build();
protected final Map<String, Object> getSettings() {
return this.settings;
}
@SuppressWarnings("unchecked")
protected final B getThis() {
return (B) this;
}
}
}

View File

@ -1,80 +0,0 @@
package run.halo.app.infra.config;
/**
* The names for all the configuration settings.
*
* @author guqing
* @date 2022-04-14
*/
public class ConfigurationSettingNames {
private static final String SETTINGS_NAMESPACE = "settings.";
private ConfigurationSettingNames() {
}
/**
* The names for provider configuration settings.
*/
public static final class Provider {
private static final String PROVIDER_SETTINGS_NAMESPACE =
SETTINGS_NAMESPACE.concat("provider.");
/**
* Set the URL the Provider uses as its Issuer Identifier.
*/
public static final String ISSUER = PROVIDER_SETTINGS_NAMESPACE.concat("issuer");
/**
* Set the Provider's OAuth 2.0 Authorization endpoint.
*/
public static final String AUTHORIZATION_ENDPOINT =
PROVIDER_SETTINGS_NAMESPACE.concat("authorization-endpoint");
/**
* Set the Provider's OAuth 2.0 Token endpoint.
*/
public static final String TOKEN_ENDPOINT =
PROVIDER_SETTINGS_NAMESPACE.concat("token-endpoint");
/**
* Set the Provider's JWK Set endpoint.
*/
public static final String JWK_SET_ENDPOINT =
PROVIDER_SETTINGS_NAMESPACE.concat("jwk-set-endpoint");
private Provider() {
}
}
/**
* The names for token configuration settings.
*/
public static final class Token {
private static final String TOKEN_SETTINGS_NAMESPACE = SETTINGS_NAMESPACE.concat("token.");
/**
* Set the time-to-live for an access token.
*/
public static final String ACCESS_TOKEN_TIME_TO_LIVE =
TOKEN_SETTINGS_NAMESPACE.concat("access-token-time-to-live");
/**
* Set to {@code true} if refresh tokens are reused when returning the access token
* response,
* or {@code false} if a new refresh token is issued.
*/
public static final String REUSE_REFRESH_TOKENS =
TOKEN_SETTINGS_NAMESPACE.concat("reuse-refresh-tokens");
/**
* Set the time-to-live for a refresh token.
*/
public static final String REFRESH_TOKEN_TIME_TO_LIVE =
TOKEN_SETTINGS_NAMESPACE.concat("refresh-token-time-to-live");
private Token() {
}
}
}

View File

@ -9,9 +9,4 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "halo")
public class HaloProperties {
private final JwtProperties jwt = new JwtProperties();
public JwtProperties getJwt() {
return jwt;
}
}

View File

@ -1,5 +1,6 @@
package run.halo.app.infra.properties;
import jakarta.validation.constraints.NotNull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@ -10,14 +11,18 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.core.io.Resource;
import org.springframework.security.converter.RsaKeyConverters;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.validation.annotation.Validated;
/**
* @author guqing
* @author johnniang
* @date 2022-04-12
*/
@ConfigurationProperties(prefix = "halo.security.oauth2.jwt")
@Validated
public class JwtProperties {
/**
@ -29,15 +34,37 @@ public class JwtProperties {
/**
* JSON Web Algorithm used for verifying the digital signatures.
*/
private String jwsAlgorithm = "RS256";
private SignatureAlgorithm jwsAlgorithm;
/**
* Location of the file containing the public key used to verify a JWT.
*/
@NotNull
private Resource publicKeyLocation;
@NotNull
private Resource privateKeyLocation;
private final RSAPrivateKey privateKey;
private final RSAPublicKey publicKey;
public JwtProperties(String issuerUri, SignatureAlgorithm jwsAlgorithm,
Resource publicKeyLocation,
Resource privateKeyLocation) throws IOException {
this.issuerUri = issuerUri;
this.jwsAlgorithm = jwsAlgorithm;
if (jwsAlgorithm == null) {
this.jwsAlgorithm = SignatureAlgorithm.RS256;
}
this.publicKeyLocation = publicKeyLocation;
this.privateKeyLocation = privateKeyLocation;
//TODO initialize private and public keys at first startup.
this.privateKey = this.readPrivateKey();
this.publicKey = this.readPublicKey();
}
public String getIssuerUri() {
return issuerUri;
}
@ -46,11 +73,11 @@ public class JwtProperties {
this.issuerUri = issuerUri;
}
public String getJwsAlgorithm() {
public SignatureAlgorithm getJwsAlgorithm() {
return this.jwsAlgorithm;
}
public void setJwsAlgorithm(String jwsAlgorithm) {
public void setJwsAlgorithm(SignatureAlgorithm jwsAlgorithm) {
this.jwsAlgorithm = jwsAlgorithm;
}
@ -70,7 +97,15 @@ public class JwtProperties {
this.privateKeyLocation = privateKeyLocation;
}
public RSAPublicKey readPublicKey() throws IOException {
public RSAPrivateKey getPrivateKey() {
return privateKey;
}
public RSAPublicKey getPublicKey() {
return publicKey;
}
private RSAPublicKey readPublicKey() throws IOException {
String key = "halo.security.oauth2.jwt.public-key-location";
Assert.notNull(this.publicKeyLocation, "PublicKeyLocation must not be null");
if (!this.publicKeyLocation.exists()) {
@ -84,7 +119,7 @@ public class JwtProperties {
}
}
public RSAPrivateKey readPrivateKey() throws IOException {
private RSAPrivateKey readPrivateKey() throws IOException {
String key = "halo.security.oauth2.jwt.private-key-location";
Assert.notNull(this.privateKeyLocation, "PrivateKeyLocation must not be null");
if (!this.privateKeyLocation.exists()) {

View File

@ -1,10 +1,8 @@
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;
@ -13,14 +11,6 @@ import org.springframework.util.StreamUtils;
* @date 2022-04-12
*/
public class HaloUtils {
public static boolean isAjaxRequest(HttpServletRequest request) {
String requestedWith = request.getHeader("x-requested-with");
return requestedWith == null || requestedWith.equalsIgnoreCase("XMLHttpRequest");
}
public static String simpleUUID() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* <p>Read the file under the classpath as a string.</p>

View File

@ -18,7 +18,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
/**
* Plugin autoconfiguration for Spring Boot.

View File

@ -8,7 +8,7 @@ import org.pf4j.PluginWrapper;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
/**
* Plugin mapping manager.

View File

@ -0,0 +1,37 @@
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;
}
}

View File

@ -0,0 +1,31 @@
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));
}
}

View File

@ -0,0 +1,35 @@
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.CodecConfigurer;
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;
public class LoginAuthenticationFilter extends AuthenticationWebFilter {
public LoginAuthenticationFilter(LoginAuthenticationManager authenticationManager,
CodecConfigurer codec,
JwtEncoder jwtEncoder,
JwtProperties jwtProp,
ServerResponse.Context responseContext) {
super(authenticationManager);
var loginMatcher = new AndServerWebExchangeMatcher(
pathMatchers(HttpMethod.POST, "/api/auth/token"),
new MediaTypeServerWebExchangeMatcher(MediaType.APPLICATION_JSON)
);
setRequiresAuthenticationMatcher(loginMatcher);
setServerAuthenticationConverter(new LoginAuthenticationConverter(codec.getReaders()));
setAuthenticationSuccessHandler(
new LoginAuthenticationSuccessHandler(jwtEncoder, jwtProp, responseContext));
setAuthenticationFailureHandler(new LoginAuthenticationFailureHandler(responseContext));
}
}

View File

@ -0,0 +1,19 @@
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);
}
}
}

View File

@ -0,0 +1,62 @@
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));
}
}

View File

@ -0,0 +1,31 @@
package run.halo.app.security.authentication.pat;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
public class PatAuthenticationConverter implements ServerAuthenticationConverter {
private final ServerBearerTokenAuthenticationConverter bearerTokenConverter;
public PatAuthenticationConverter() {
bearerTokenConverter = new ServerBearerTokenAuthenticationConverter();
}
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return bearerTokenConverter.convert(exchange)
.filter(token -> token instanceof BearerTokenAuthenticationToken)
.cast(BearerTokenAuthenticationToken.class)
.filter(this::isPersonalAccessToken)
.cast(Authentication.class);
}
private boolean isPersonalAccessToken(BearerTokenAuthenticationToken bearerToken) {
String token = bearerToken.getToken();
return token.startsWith("pat_");
}
}

View File

@ -0,0 +1,14 @@
package run.halo.app.security.authentication.pat;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import reactor.core.publisher.Mono;
public class PatAuthenticationManager implements ReactiveAuthenticationManager {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
// TODO Implement personal access token authentication.
return null;
}
}

View File

@ -0,0 +1,39 @@
package run.halo.app.security.authentication.pat;
import java.time.Instant;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@GVK(group = "",
version = "v1alpha1",
kind = "PersonalAccessToken",
singular = "personalaccesstoken",
plural = "personalaccesstokens")
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class PersonalAccessToken extends AbstractExtension {
private PersonalAccessTokenSpec spec;
@Data
public static class PersonalAccessTokenSpec {
private String userName;
private String displayName;
private Boolean revoked;
private Instant expiresAt;
private String scopes;
private String tokenDigest;
}
}

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import org.springframework.security.core.userdetails.UserDetails;

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import org.springframework.security.core.userdetails.UserDetails;

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import org.springframework.security.core.userdetails.UserDetails;

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import java.util.ArrayList;
import java.util.List;

View File

@ -1,12 +1,10 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import java.util.Collections;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
/**
@ -27,21 +25,15 @@ public class DefaultRoleBindingLister implements RoleBindingLister {
private static final String ROLE_AUTHORITY_PREFIX = "ROLE_";
@Override
public Set<String> listBoundRoleNames() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (authentication == null) {
log.debug("No authentication found in SecurityContext.");
return Collections.emptySet();
}
return authentication.getAuthorities()
.stream()
public Set<String> listBoundRoleNames(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream()
.map(GrantedAuthority::getAuthority)
// Exclude anonymous user roles
.filter(authority -> !authority.equals("ROLE_ANONYMOUS"))
.map(scope -> {
if (scope.startsWith(SCOPE_AUTHORITY_PREFIX)) {
return scope.replaceFirst(SCOPE_AUTHORITY_PREFIX, "");
scope = scope.replaceFirst(SCOPE_AUTHORITY_PREFIX, "");
// keep checking the ROLE_ here
}
if (scope.startsWith(ROLE_AUTHORITY_PREFIX)) {
return scope.replaceFirst(ROLE_AUTHORITY_PREFIX, "");

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import java.util.Collections;
import java.util.List;
@ -39,7 +39,7 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
@Override
public void visitRulesFor(UserDetails user, RuleAccumulator visitor) {
Set<String> roleNames = roleBindingLister.listBoundRoleNames();
Set<String> roleNames = roleBindingLister.listBoundRoleNames(user.getAuthorities());
List<PolicyRule> rules = Collections.emptyList();
for (String roleName : roleNames) {

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import lombok.Data;

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import java.util.ArrayList;
import java.util.LinkedList;

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import java.util.List;
import java.util.Objects;

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import java.util.Objects;
import lombok.Getter;

View File

@ -1,79 +1,49 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Mono;
/**
* @author guqing
* @since 2.0.0
*/
@Slf4j
public class RequestInfoAuthorizationManager
implements AuthorizationManager<RequestAuthorizationContext> {
implements ReactiveAuthorizationManager<AuthorizationContext> {
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private AuthorizationRuleResolver ruleResolver;
private final AuthorizationRuleResolver ruleResolver;
public RequestInfoAuthorizationManager(RoleGetter roleGetter) {
this.ruleResolver = new DefaultRuleResolver(roleGetter);
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authenticationSupplier,
RequestAuthorizationContext requestContext) {
HttpServletRequest request = requestContext.getRequest();
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication,
AuthorizationContext context) {
ServerHttpRequest request = context.getExchange().getRequest();
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
Authentication authentication = authenticationSupplier.get();
UserDetails userDetails = createUserDetails(authentication);
AttributesRecord attributes = new AttributesRecord(userDetails, requestInfo);
// visitor rules
AuthorizingVisitor authorizingVisitor = new AuthorizingVisitor(attributes);
ruleResolver.visitRulesFor(userDetails, authorizingVisitor);
if (!authorizingVisitor.isAllowed()) {
// print errors
showErrorMessage(authorizingVisitor.getErrors());
return new AuthorizationDecision(false);
}
return new AuthorizationDecision(isGranted(authentication));
}
private void showErrorMessage(List<Throwable> errors) {
if (CollectionUtils.isEmpty(errors)) {
return;
}
for (Throwable error : errors) {
log.error("Access decision error: ", error);
}
}
private UserDetails createUserDetails(Authentication authentication) {
Assert.notNull(authentication, "The authentication must not be null.");
return User.withUsername(authentication.getName())
.authorities(authentication.getAuthorities())
.password("N/A")
.build();
}
public void setRuleResolver(AuthorizationRuleResolver ruleResolver) {
Assert.notNull(ruleResolver, "ruleResolver must not be null.");
this.ruleResolver = ruleResolver;
return authentication.map(auth -> {
UserDetails userDetails = this.createUserDetails(auth);
var record = new AttributesRecord(userDetails, requestInfo);
var visitor = new AuthorizingVisitor(record);
ruleResolver.visitRulesFor(userDetails, visitor);
if (!visitor.isAllowed()) {
showErrorMessage(visitor.getErrors());
return new AuthorizationDecision(false);
}
return new AuthorizationDecision(isGranted(auth));
});
}
private boolean isGranted(Authentication authentication) {
@ -85,4 +55,21 @@ public class RequestInfoAuthorizationManager
return !this.trustResolver.isAnonymous(authentication);
}
private UserDetails createUserDetails(Authentication authentication) {
Assert.notNull(authentication, "The authentication must not be null.");
return User.withUsername(authentication.getName())
.authorities(authentication.getAuthorities())
.password("")
.build();
}
private void showErrorMessage(List<Throwable> errors) {
if (CollectionUtils.isEmpty(errors)) {
return;
}
for (Throwable error : errors) {
log.error("Access decision error: ", error);
}
}
}

View File

@ -1,10 +1,11 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.server.PathContainer;
import org.springframework.http.server.reactive.ServerHttpRequest;
/**
* @author guqing
@ -82,15 +83,14 @@ public class RequestInfoFactory {
* @param request http request
* @return request holds the information of both resource and non-resource requests
*/
public RequestInfo newRequestInfo(HttpServletRequest request) {
public RequestInfo newRequestInfo(ServerHttpRequest request) {
// non-resource request default
RequestInfo requestInfo = new RequestInfo(
false,
request.getRequestURI(),
StringUtils.lowerCase(request.getMethod())
);
PathContainer path = request.getPath().pathWithinApplication();
RequestInfo requestInfo =
new RequestInfo(false, path.value(), request.getMethod().name().toLowerCase());
String[] currentParts = splitPath(request.getPath().value());
String[] currentParts = splitPath(request.getRequestURI());
if (currentParts.length < 3) {
// return a non-resource request
return requestInfo;
@ -123,12 +123,12 @@ public class RequestInfoFactory {
if (currentParts.length < 2) {
throw new IllegalArgumentException(
String.format("unable to determine kind and namespace from url, %s",
request.getRequestURI()));
request.getPath()));
}
requestInfo.verb = currentParts[0];
currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length);
} else {
requestInfo.verb = switch (request.getMethod()) {
requestInfo.verb = switch (request.getMethod().name().toUpperCase()) {
case "POST" -> "create";
case "GET", "HEAD" -> "get";
case "PUT" -> "update";
@ -175,7 +175,7 @@ public class RequestInfoFactory {
// if there's no name on the request and we thought it was a get before, then the actual
// verb is a list or a watch
if (requestInfo.name.length() == 0 && "get".equals(requestInfo.verb)) {
String watch = request.getParameter("watch");
var watch = request.getQueryParams().getFirst("watch");
if (isWatch(watch)) {
requestInfo.verb = "watch";
} else {
@ -191,6 +191,10 @@ public class RequestInfoFactory {
return requestInfo;
}
boolean isWatch(String requestParam) {
return "1".equals(requestParam) || "true".equals(requestParam);
}
public String[] splitPath(String path) {
path = StringUtils.strip(path, "/");
if (StringUtils.isEmpty(path)) {
@ -198,9 +202,4 @@ public class RequestInfoFactory {
}
return StringUtils.split(path, "/");
}
boolean isWatch(String requestParam) {
return "1".equals(requestParam)
|| "true".equals(requestParam);
}
}

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import java.util.List;
import lombok.Data;

View File

@ -0,0 +1,15 @@
package run.halo.app.security.authorization;
import java.util.Collection;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
/**
* @author guqing
* @since 2.0.0
*/
@FunctionalInterface
public interface RoleBindingLister {
Set<String> listBoundRoleNames(Collection<? extends GrantedAuthority> authorities);
}

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import org.springframework.lang.NonNull;

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
import lombok.Data;

View File

@ -1,4 +1,4 @@
package run.halo.app.identity.authorization;
package run.halo.app.security.authorization;
/**
* @author guqing

Some files were not shown because too many files have changed in this diff Show More