feat: rewrite SecurityConfig — single filter chain, Logto OAuth2 Resource Server
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,104 +1,98 @@
|
|||||||
package net.siegeln.cameleer.saas.config;
|
package net.siegeln.cameleer.saas.config;
|
||||||
|
|
||||||
import net.siegeln.cameleer.saas.auth.JwtAuthenticationFilter;
|
import com.nimbusds.jose.JWSAlgorithm;
|
||||||
|
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
|
||||||
|
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
|
||||||
|
import com.nimbusds.jose.proc.SecurityContext;
|
||||||
|
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
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.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
||||||
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
|
||||||
|
|
||||||
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
|
|
||||||
import com.nimbusds.jose.proc.SecurityContext;
|
|
||||||
import com.nimbusds.jose.util.ResourceRetriever;
|
|
||||||
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
|
|
||||||
import com.nimbusds.jose.JWSAlgorithm;
|
|
||||||
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Set;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
private final JwtAuthenticationFilter machineTokenFilter;
|
|
||||||
private final TenantResolutionFilter tenantResolutionFilter;
|
private final TenantResolutionFilter tenantResolutionFilter;
|
||||||
|
|
||||||
public SecurityConfig(JwtAuthenticationFilter machineTokenFilter, TenantResolutionFilter tenantResolutionFilter) {
|
public SecurityConfig(TenantResolutionFilter tenantResolutionFilter) {
|
||||||
this.machineTokenFilter = machineTokenFilter;
|
|
||||||
this.tenantResolutionFilter = tenantResolutionFilter;
|
this.tenantResolutionFilter = tenantResolutionFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Order(1)
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
public SecurityFilterChain machineAuthFilterChain(HttpSecurity http) throws Exception {
|
|
||||||
http
|
|
||||||
.securityMatcher("/api/agent/**", "/api/license/verify/**")
|
|
||||||
.csrf(csrf -> csrf.disable())
|
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
||||||
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
|
|
||||||
.addFilterBefore(machineTokenFilter, UsernamePasswordAuthenticationFilter.class);
|
|
||||||
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@Order(2)
|
|
||||||
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
|
|
||||||
http
|
http
|
||||||
.csrf(csrf -> csrf.disable())
|
.csrf(csrf -> csrf.disable())
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/actuator/health").permitAll()
|
.requestMatchers("/actuator/health").permitAll()
|
||||||
.requestMatchers("/auth/verify").permitAll()
|
|
||||||
.requestMatchers("/api/config").permitAll()
|
.requestMatchers("/api/config").permitAll()
|
||||||
.requestMatchers("/", "/index.html", "/login", "/callback", "/environments/**", "/license", "/admin/**").permitAll()
|
.requestMatchers("/", "/index.html", "/login", "/callback",
|
||||||
|
"/environments/**", "/license", "/admin/**").permitAll()
|
||||||
.requestMatchers("/assets/**", "/favicon.ico").permitAll()
|
.requestMatchers("/assets/**", "/favicon.ico").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> {}))
|
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt ->
|
||||||
.addFilterBefore(machineTokenFilter, UsernamePasswordAuthenticationFilter.class)
|
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())))
|
||||||
.addFilterAfter(tenantResolutionFilter, BearerTokenAuthenticationFilter.class);
|
.addFilterAfter(tenantResolutionFilter, BearerTokenAuthenticationFilter.class);
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JwtAuthenticationConverter jwtAuthenticationConverter() {
|
||||||
|
var converter = new JwtAuthenticationConverter();
|
||||||
|
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
|
||||||
|
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||||
|
|
||||||
|
var roles = jwt.getClaimAsStringList("roles");
|
||||||
|
if (roles != null) {
|
||||||
|
roles.forEach(r -> authorities.add(new SimpleGrantedAuthority("ROLE_" + r)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgRoles = jwt.getClaimAsStringList("organization_roles");
|
||||||
|
if (orgRoles != null) {
|
||||||
|
orgRoles.forEach(r -> authorities.add(new SimpleGrantedAuthority("ROLE_org_" + r)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorities;
|
||||||
|
});
|
||||||
|
return converter;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public JwtDecoder jwtDecoder(
|
public JwtDecoder jwtDecoder(
|
||||||
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwkSetUri,
|
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwkSetUri,
|
||||||
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri:}") String issuerUri) throws Exception {
|
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri:}") String issuerUri) throws Exception {
|
||||||
// Logto issues tokens with typ "at+jwt" (RFC 9068). Spring Security's default
|
|
||||||
// decoder only allows typ "JWT". Build a custom processor that accepts both.
|
|
||||||
var jwkSource = JWKSourceBuilder.create(new URL(jwkSetUri)).build();
|
var jwkSource = JWKSourceBuilder.create(new URL(jwkSetUri)).build();
|
||||||
var keySelector = new JWSVerificationKeySelector<SecurityContext>(
|
var keySelector = new JWSVerificationKeySelector<SecurityContext>(
|
||||||
JWSAlgorithm.ES384, jwkSource);
|
JWSAlgorithm.ES384, jwkSource);
|
||||||
|
|
||||||
var jwtProcessor = new DefaultJWTProcessor<SecurityContext>();
|
var processor = new DefaultJWTProcessor<SecurityContext>();
|
||||||
jwtProcessor.setJWSKeySelector(keySelector);
|
processor.setJWSKeySelector(keySelector);
|
||||||
// Allow both "JWT" and "at+jwt" token types
|
processor.setJWSTypeVerifier((type, context) -> { /* accept JWT and at+jwt */ });
|
||||||
jwtProcessor.setJWSTypeVerifier((type, context) -> { /* accept any type */ });
|
|
||||||
|
|
||||||
var decoder = new NimbusJwtDecoder(jwtProcessor);
|
var decoder = new NimbusJwtDecoder(processor);
|
||||||
if (issuerUri != null && !issuerUri.isEmpty()) {
|
if (issuerUri != null && !issuerUri.isEmpty()) {
|
||||||
decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri));
|
decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri));
|
||||||
}
|
}
|
||||||
return decoder;
|
return decoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public PasswordEncoder passwordEncoder() {
|
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,6 @@ management:
|
|||||||
show-details: when-authorized
|
show-details: when-authorized
|
||||||
|
|
||||||
cameleer:
|
cameleer:
|
||||||
jwt:
|
|
||||||
expiration: 86400 # 24 hours in seconds
|
|
||||||
private-key-path: ${CAMELEER_JWT_PRIVATE_KEY_PATH:}
|
|
||||||
public-key-path: ${CAMELEER_JWT_PUBLIC_KEY_PATH:}
|
|
||||||
identity:
|
identity:
|
||||||
logto-endpoint: ${LOGTO_ENDPOINT:}
|
logto-endpoint: ${LOGTO_ENDPOINT:}
|
||||||
logto-public-endpoint: ${LOGTO_PUBLIC_ENDPOINT:}
|
logto-public-endpoint: ${LOGTO_PUBLIC_ENDPOINT:}
|
||||||
@@ -49,7 +45,6 @@ cameleer:
|
|||||||
deployment-thread-pool-size: 4
|
deployment-thread-pool-size: 4
|
||||||
container-memory-limit: ${CAMELEER_CONTAINER_MEMORY_LIMIT:512m}
|
container-memory-limit: ${CAMELEER_CONTAINER_MEMORY_LIMIT:512m}
|
||||||
container-cpu-shares: ${CAMELEER_CONTAINER_CPU_SHARES:512}
|
container-cpu-shares: ${CAMELEER_CONTAINER_CPU_SHARES:512}
|
||||||
bootstrap-token: ${CAMELEER_AUTH_TOKEN:}
|
|
||||||
cameleer3-server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081}
|
cameleer3-server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081}
|
||||||
domain: ${DOMAIN:localhost}
|
domain: ${DOMAIN:localhost}
|
||||||
clickhouse:
|
clickhouse:
|
||||||
|
|||||||
Reference in New Issue
Block a user