All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apereo.cas.config.CasWebSecurityConfiguration Maven / Gradle / Ivy

The newest version!
package org.apereo.cas.config;

import org.apereo.cas.authentication.support.password.PasswordEncoderUtils;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.configuration.features.CasFeatureModule;
import org.apereo.cas.configuration.support.JpaBeans;
import org.apereo.cas.util.CollectionUtils;
import org.apereo.cas.util.crypto.DefaultPasswordEncoder;
import org.apereo.cas.util.serialization.JacksonObjectMapperFactory;
import org.apereo.cas.util.spring.boot.ConditionalOnFeatureEnabled;
import org.apereo.cas.web.CasWebSecurityConfigurer;
import org.apereo.cas.web.flow.CasWebflowConstants;
import org.apereo.cas.web.security.CasWebSecurityConfigurerAdapter;
import org.apereo.cas.web.security.CasWebflowSecurityContextRepository;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.apereo.inspektr.common.web.ClientInfoExtractionOptions;
import org.apereo.inspektr.common.web.ClientInfoThreadLocalFilter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.Ordered;
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.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.webflow.context.servlet.FlowUrlHandler;
import org.springframework.webflow.executor.FlowExecutor;
import jakarta.annotation.Nonnull;
import java.io.Serial;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Pattern;

/**
 * This is {@link CasWebSecurityConfiguration}.
 *
 * @author Misagh Moayyed
 * @since 6.0.0
 */
@EnableConfigurationProperties(CasConfigurationProperties.class)
@ConditionalOnFeatureEnabled(feature = CasFeatureModule.FeatureCatalog.WebApplication)
@Configuration(value = "CasWebSecurityConfiguration", proxyBeanMethods = false)
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@EnableWebSecurity
class CasWebSecurityConfiguration {

    @Bean
    @Lazy(false)
    public InitializingBean securityContextHolderInitialization() {
        return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_THREADLOCAL);
    }

    @Configuration(value = "CasWebAppSecurityMvcConfiguration", proxyBeanMethods = false)
    @EnableConfigurationProperties(CasConfigurationProperties.class)
    static class CasWebAppSecurityMvcConfiguration {
        @Bean
        @ConditionalOnMissingBean(name = "casWebAppSecurityWebMvcConfigurer")
        public WebMvcConfigurer casWebAppSecurityWebMvcConfigurer(final CasConfigurationProperties casProperties) {
            return new WebMvcConfigurer() {
                @Override
                public void addViewControllers(@Nonnull final ViewControllerRegistry registry) {
                    if (casProperties.getMonitor().getEndpoints().isFormLoginEnabled()) {
                        registry.addViewController(CasWebSecurityConfigurer.ENDPOINT_URL_ADMIN_FORM_LOGIN)
                            .setViewName(CasWebflowConstants.VIEW_ID_ENDPOINT_ADMIN_LOGIN_VIEW);
                        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
                    }
                }
            };
        }
    }

    @Configuration(value = "CasWebappCoreSecurityConfiguration", proxyBeanMethods = false)
    @EnableConfigurationProperties(CasConfigurationProperties.class)
    static class CasWebappCoreSecurityConfiguration {

        @Bean
        @ConditionalOnMissingBean(name = "securityContextRepository")
        @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
        public SecurityContextRepository securityContextRepository(
            final ConfigurableApplicationContext applicationContext,
            @Qualifier("loginFlowUrlHandler") final FlowUrlHandler loginFlowUrlHandler,
            @Qualifier("loginFlowExecutor") final FlowExecutor loginFlowExecutor) {
            return new DelegatingSecurityContextRepository(
                new RequestAttributeSecurityContextRepository(),
                new HttpSessionSecurityContextRepository(),
                new CasWebflowSecurityContextRepository(applicationContext)
            );
        }

        @Bean
        @ConditionalOnMissingBean(name = "casClientInfoLoggingFilter")
        public FilterRegistrationBean casClientInfoLoggingFilter(
            final CasConfigurationProperties casProperties) {
            val bean = new FilterRegistrationBean();
            val audit = casProperties.getAudit().getEngine();
            val options = ClientInfoExtractionOptions.builder()
                .alternateLocalAddrHeaderName(audit.getAlternateClientAddrHeaderName())
                .alternateServerAddrHeaderName(audit.getAlternateServerAddrHeaderName())
                .useServerHostAddress(audit.isUseServerHostAddress())
                .httpRequestHeaders(audit.getHttpRequestHeaders())
                .build();
            bean.setFilter(new ClientInfoThreadLocalFilter(options));
            bean.setUrlPatterns(CollectionUtils.wrap("/*"));
            bean.setName("CAS Client Info Logging Filter");
            bean.setAsyncSupported(true);
            bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
            return bean;
        }

        @Bean
        public FilterRegistrationBean securityContextHolderFilter(
            @Qualifier("securityContextRepository") final SecurityContextRepository securityContextRepository) {
            val bean = new FilterRegistrationBean();
            bean.setFilter(new SecurityContextHolderFilter(securityContextRepository));
            bean.setUrlPatterns(CollectionUtils.wrap("/*"));
            bean.setName("Spring Security Context Holder Filter");
            bean.setAsyncSupported(true);
            bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
            return bean;
        }

        @Bean
        @ConditionalOnMissingBean(name = "casWebSecurityCustomizer")
        public WebSecurityCustomizer casWebSecurityCustomizer(
            @Qualifier("securityContextRepository") final SecurityContextRepository securityContextRepository,
            final ObjectProvider pathMappedEndpoints,
            final List configurersList,
            final WebEndpointProperties webEndpointProperties,
            final CasConfigurationProperties casProperties,
            final WebProperties webProperties) {
            val adapter = new CasWebSecurityConfigurerAdapter(casProperties,
                webEndpointProperties, pathMappedEndpoints, configurersList,
                securityContextRepository, webProperties);
            return adapter::configureWebSecurity;
        }

        @Bean
        @ConditionalOnMissingBean(name = "casWebSecurityConfigurerAdapter")
        public SecurityFilterChain casWebSecurityConfigurerAdapter(
            @Qualifier("securityContextRepository") final SecurityContextRepository securityContextRepository,
            final HttpSecurity http,
            final ObjectProvider pathMappedEndpoints,
            final List configurersList,
            final WebEndpointProperties webEndpointProperties,
            final SecurityProperties securityProperties,
            final CasConfigurationProperties casProperties,
            final WebProperties webProperties) throws Exception {
            val adapter = new CasWebSecurityConfigurerAdapter(casProperties,
                webEndpointProperties, pathMappedEndpoints, configurersList,
                securityContextRepository, webProperties);
            return adapter.configureHttpSecurity(http).build();
        }
    }

    @Configuration(value = "CasWebAppSecurityJdbcConfiguration", proxyBeanMethods = false)
    @EnableConfigurationProperties(CasConfigurationProperties.class)
    @ConditionalOnProperty(name = "cas.monitor.endpoints.jdbc.query")
    @SuppressWarnings("ConditionalOnProperty")
    static class CasWebAppSecurityJdbcConfiguration {
        @Bean
        @ConditionalOnMissingBean(name = "jdbcUserDetailsPasswordEncoder")
        public static PasswordEncoder jdbcUserDetailsPasswordEncoder(
            final CasConfigurationProperties casProperties,
            final ConfigurableApplicationContext applicationContext) {
            val jdbc = casProperties.getMonitor().getEndpoints().getJdbc();
            return PasswordEncoderUtils.newPasswordEncoder(jdbc.getPasswordEncoder(), applicationContext);
        }

        @Bean
        @ConditionalOnMissingBean(name = "jdbcUserDetailsManager")
        public UserDetailsManager jdbcUserDetailsManager(
            final CasConfigurationProperties casProperties) {
            val jdbc = casProperties.getMonitor().getEndpoints().getJdbc();
            val manager = new JdbcUserDetailsManager(JpaBeans.newDataSource(jdbc));
            manager.setRolePrefix(jdbc.getRolePrefix());
            manager.setUsersByUsernameQuery(jdbc.getQuery());
            return manager;
        }
    }

    @Configuration(value = "CasWebAppSecurityJsonUsersConfiguration", proxyBeanMethods = false)
    @EnableConfigurationProperties(CasConfigurationProperties.class)
    @ConditionalOnProperty(name = "cas.monitor.endpoints.json.location")
    @SuppressWarnings("ConditionalOnProperty")
    static class CasWebAppSecurityJsonUsersConfiguration {
        private static final ObjectMapper MAPPER = JacksonObjectMapperFactory.builder()
            .defaultTypingEnabled(false).build().toObjectMapper();
        
        private static final Pattern PATTERN_PASSWORD_ALG = Pattern.compile("^\\{.+\\}.*");

        @Bean
        @ConditionalOnMissingBean(name = "jsonUserDetailsService")
        public UserDetailsService userDetailsService(final CasConfigurationProperties casProperties) throws Exception {
            val resource = casProperties.getMonitor().getEndpoints().getJson().getLocation();
            val listOfUsers = MAPPER.readValue(resource.getInputStream(), new TypeReference>() {
            });
            val userDetails = listOfUsers
                .stream()
                .map(user -> {
                    val authorities = user.getAuthorities()
                        .stream()
                        .map(authority -> StringUtils.prependIfMissing(authority, "ROLE_"))
                        .map(SimpleGrantedAuthority::new)
                        .toList();
                    var password = user.getPassword();
                    if (!PATTERN_PASSWORD_ALG.matcher(password).matches()) {
                        password = "{noop}" + password;
                    }
                    return User.builder()
                        .username(user.getUsername())
                        .password(password)
                        .authorities(authorities)
                        .build();
                })
                .toList();
            return new InMemoryUserDetailsManager(userDetails);
        }

        @Bean
        @ConditionalOnMissingBean(name = "jsonUserDetailsPasswordEncoder")
        public PasswordEncoder jsonUserDetailsPasswordEncoder() {
            val encoders = new HashMap();
            encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
            encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
            encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
            encoders.put("bcrypt", new BCryptPasswordEncoder());
            encoders.put("sha256", new DefaultPasswordEncoder("SHA-256", StandardCharsets.UTF_8.name()));
            encoders.put("sha512", new DefaultPasswordEncoder("SHA-512", StandardCharsets.UTF_8.name()));
            encoders.put("noop", NoOpPasswordEncoder.getInstance());
            return new DelegatingPasswordEncoder("sha512", encoders);
        }

        @Getter
        @Setter
        @NoArgsConstructor
        private static final class CasUserDetails implements Serializable {
            @Serial
            private static final long serialVersionUID = -741527534790033702L;

            private String username;
            private String password;
            private List authorities = new ArrayList<>();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy