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;
|
||||
|
||||
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.context.annotation.Bean;
|
||||
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.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
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.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.util.Set;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter machineTokenFilter;
|
||||
private final TenantResolutionFilter tenantResolutionFilter;
|
||||
|
||||
public SecurityConfig(JwtAuthenticationFilter machineTokenFilter, TenantResolutionFilter tenantResolutionFilter) {
|
||||
this.machineTokenFilter = machineTokenFilter;
|
||||
public SecurityConfig(TenantResolutionFilter tenantResolutionFilter) {
|
||||
this.tenantResolutionFilter = tenantResolutionFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
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 {
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/actuator/health").permitAll()
|
||||
.requestMatchers("/auth/verify").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()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> {}))
|
||||
.addFilterBefore(machineTokenFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt ->
|
||||
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())))
|
||||
.addFilterAfter(tenantResolutionFilter, BearerTokenAuthenticationFilter.class);
|
||||
|
||||
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
|
||||
public JwtDecoder jwtDecoder(
|
||||
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwkSetUri,
|
||||
@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 keySelector = new JWSVerificationKeySelector<SecurityContext>(
|
||||
JWSAlgorithm.ES384, jwkSource);
|
||||
|
||||
var jwtProcessor = new DefaultJWTProcessor<SecurityContext>();
|
||||
jwtProcessor.setJWSKeySelector(keySelector);
|
||||
// Allow both "JWT" and "at+jwt" token types
|
||||
jwtProcessor.setJWSTypeVerifier((type, context) -> { /* accept any type */ });
|
||||
var processor = new DefaultJWTProcessor<SecurityContext>();
|
||||
processor.setJWSKeySelector(keySelector);
|
||||
processor.setJWSTypeVerifier((type, context) -> { /* accept JWT and at+jwt */ });
|
||||
|
||||
var decoder = new NimbusJwtDecoder(jwtProcessor);
|
||||
var decoder = new NimbusJwtDecoder(processor);
|
||||
if (issuerUri != null && !issuerUri.isEmpty()) {
|
||||
decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri));
|
||||
}
|
||||
return decoder;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,6 @@ management:
|
||||
show-details: when-authorized
|
||||
|
||||
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:
|
||||
logto-endpoint: ${LOGTO_ENDPOINT:}
|
||||
logto-public-endpoint: ${LOGTO_PUBLIC_ENDPOINT:}
|
||||
@@ -49,7 +45,6 @@ cameleer:
|
||||
deployment-thread-pool-size: 4
|
||||
container-memory-limit: ${CAMELEER_CONTAINER_MEMORY_LIMIT:512m}
|
||||
container-cpu-shares: ${CAMELEER_CONTAINER_CPU_SHARES:512}
|
||||
bootstrap-token: ${CAMELEER_AUTH_TOKEN:}
|
||||
cameleer3-server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081}
|
||||
domain: ${DOMAIN:localhost}
|
||||
clickhouse:
|
||||
|
||||
Reference in New Issue
Block a user