mirror of https://github.com/halo-dev/halo
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 utilitypull/2132/head
parent
19db04c430
commit
1024f71635
15
build.gradle
15
build.gradle
|
@ -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') {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
@startuml
|
||||
ExceptionHandlingWebHandler -> FilteringWebHandler
|
||||
FilteringWebHandler contains filters and DispatcherHandler
|
||||
@enduml
|
|
@ -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) {
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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!')";
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 "Introspected" 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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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())));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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<Jwt, Collection<GrantedAuthority>>}
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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_");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
package run.halo.app.security.authorization;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
package run.halo.app.security.authorization;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
package run.halo.app.security.authorization;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
package run.halo.app.security.authorization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
|
@ -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, "");
|
|
@ -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;
|
|
@ -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) {
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
package run.halo.app.security.authorization;
|
||||
|
||||
import lombok.Data;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
package run.halo.app.security.authorization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
package run.halo.app.security.authorization;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
package run.halo.app.security.authorization;
|
||||
|
||||
import java.util.Objects;
|
||||
import lombok.Getter;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
package run.halo.app.security.authorization;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
|
@ -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);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
package run.halo.app.security.authorization;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package run.halo.app.identity.authorization;
|
||||
package run.halo.app.security.authorization;
|
||||
|
||||
import lombok.Data;
|
||||
|
|
@ -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
Loading…
Reference in New Issue