diff --git a/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java b/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java index 99e4e3d..c8f72f6 100644 --- a/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java +++ b/src/main/java/net/siegeln/cameleer/saas/config/SecurityConfig.java @@ -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 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( JWSAlgorithm.ES384, jwkSource); - var jwtProcessor = new DefaultJWTProcessor(); - jwtProcessor.setJWSKeySelector(keySelector); - // Allow both "JWT" and "at+jwt" token types - jwtProcessor.setJWSTypeVerifier((type, context) -> { /* accept any type */ }); + var processor = new DefaultJWTProcessor(); + 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(); - } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b506b1c..49b29f1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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: