package com.alttd.altitudeweb.config; import com.alttd.altitudeweb.controllers.login.KeyPairService; import com.alttd.altitudeweb.model.PermissionClaimDto; 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 lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; 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.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; import java.security.KeyPair; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.*; import java.util.stream.Collectors; @Slf4j @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final KeyPairService keyPairService; private final SecurityAuthFailureHandler securityAuthFailureHandler; @Value("${chat.allowed-ip}") private String allowedIp; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { WebExpressionAuthorizationManager allowedIpCheck = new WebExpressionAuthorizationManager( "hasIpAddress('%s')".formatted(allowedIp)); return http .authorizeHttpRequests( auth -> auth .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers("/api/chat/send/**").access(allowedIpCheck) .requestMatchers("/actuator/mappings").access(allowedIpCheck) .requestMatchers("/api/form/**").authenticated() .requestMatchers("/api/login/getUsername").authenticated() .requestMatchers("/api/mail/**").authenticated() .requestMatchers("/api/site/vote").authenticated() .requestMatchers("/api/appeal").authenticated() .requestMatchers("/api/chat/read/**").hasAnyAuthority(PermissionClaimDto.HEAD_MOD.getValue(), PermissionClaimDto.MOD.getValue()) .requestMatchers("/api/site/get-staff-playtime/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) //TODO allow users access to their own folder .requestMatchers("/api/files/download/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/api/history/admin/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/api/login/userLogin/**").permitAll() .anyRequest().permitAll() ) .csrf(AbstractHttpConfigurer::disable) .oauth2ResourceServer( oauth2 -> oauth2 .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) .authenticationEntryPoint(securityAuthFailureHandler) .accessDeniedHandler(securityAuthFailureHandler) ) .exceptionHandling( ex -> ex .authenticationEntryPoint(securityAuthFailureHandler) .accessDeniedHandler(securityAuthFailureHandler) ) .sessionManagement( session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .build(); } @Bean public JwtEncoder jwtEncoder() { KeyPair keyPair = keyPairService.getJwtSigningKeyPair(); JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic()) .privateKey((RSAPrivateKey) keyPair.getPrivate()) .build(); JWKSource jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk)); return new NimbusJwtEncoder(jwkSource); } @Bean public JwtDecoder jwtDecoder() { KeyPair keyPair = keyPairService.getJwtSigningKeyPair(); return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build(); } @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(jwt -> { Map claims = jwt.getClaims(); Object authoritiesClaim = claims.get("authorities"); if (authoritiesClaim instanceof List authorities) { Collection authorityList = authorities.stream() .map(Object::toString) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); log.debug("Authorities found in authorities: {}", authorityList); return authorityList; } Object scopeClaim = claims.get("scope"); if (scopeClaim instanceof String scopeString) { Collection authorityList = Arrays.stream(scopeString.split(" ")) .map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope)) .collect(Collectors.toList()); log.debug("Authorities found in authorities scope string: {}", authorityList); return authorityList; } if (scopeClaim instanceof List scopeList) { Collection authorityList = scopeList.stream() .map(Object::toString) .map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope)) .collect(Collectors.toList()); log.debug("Authorities found in authorities scope list: {}", authorityList); return authorityList; } log.debug("No granted authorities found"); return Collections.emptyList(); }); return converter; } }