Search code examples
spring-bootspring-securityatlassian-crowd

How to use Atlassian Crowd Integration with Spring boot + Spring security using Java Beans


We use a number of spring boot projects built on top of spring security for providing small web apps for a number of purposes. We're strongly considering moving to using Crowd as a central authentication provider, but I'm having a lot of trouble getting spring boot configured to use java-style beans rather than the provided XML configuration, since that is no longer a recommended configuration (see http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#using-boot-configuration-classes for verification that java-bean style configuration is the preferred way).

My basic test-app for the security has a security configuration set up as follows:

WebSecurityConfig.java

package hello;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("user").password("password").roles("USER");
    }
}

I'm really stumped as to how to modify this to accomodate the Crowd built-in springsecurity integration. (specifically the com.atlassian.crowd:crowd-integration-springsecurity:2.8.3). the XML I was attempting to use to try and bootstrap the functionality through a standard configuration annotated class was as follows:

applicationContext-security.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:util="http://www.springframework.org/schema/util"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd
                        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

<!--
    <debug />
    <beans:alias name="springSecurityFilterChain" alias="org.springframework.security.filterChainProxy"/>   
-->

    <!-- Added for Integrating Crowd with Spring Security -->

    <!-- 3.1 Configuring Centralised User Management -->

    <!-- 3.1.1 -->
    <beans:bean id="crowdUserDetailsService" class="com.atlassian.crowd.integration.springsecurity.user.CrowdUserDetailsServiceImpl">
        <beans:property name="groupMembershipManager" ref="crowdGroupMembershipManager"/>
        <beans:property name="userManager" ref="crowdUserManager"/>
        <beans:property name="authorityPrefix" value="ROLE_"/>

<!--
        <beans:property name="groupToAuthorityMappings">
            <beans:bean factory-bean="groupToAuthorityMappings" factory-method="entrySet" />
        </beans:property>
-->
    </beans:bean>

<!--
    <util:map id="groupToAuthorityMappings">
        <beans:entry key="crowd-administrators" value="ROLE_crowd-administrators" />
        <beans:entry key="some-other-group" value="specific-authority-for-other-group" />
    </util:map>
-->

    <!-- 3.1.2 -->
    <beans:bean id="crowdAuthenticationProvider" class="com.atlassian.crowd.integration.springsecurity.RemoteCrowdAuthenticationProvider">
        <beans:constructor-arg ref="crowdAuthenticationManager"/>
        <beans:constructor-arg ref="httpAuthenticator"/>
        <beans:constructor-arg ref="crowdUserDetailsService"/>
    </beans:bean>

    <!-- 3.2 -->

    <http pattern="/console/static/session-context"
          entry-point-ref="crowdAuthenticationProcessingFilterEntryPoint">
    </http>

    <http pattern='/console/static/**' security='none'/>

    <http auto-config="false"
          entry-point-ref="crowdAuthenticationProcessingFilterEntryPoint"
          access-denied-page="/denied.html">
        <custom-filter position="FORM_LOGIN_FILTER" ref='authenticationProcessingFilter'/>
        <custom-filter position="LOGOUT_FILTER" ref='logoutFilter'/>

        <intercept-url pattern="/console/secure/**" access="ROLE_crowd-administrators"/>
        <intercept-url pattern="/console/user/**" access="IS_AUTHENTICATED_FULLY"/>

        <intercept-url pattern="/console/resource-with-own-check/**" access='IS_AUTHENTICATED_ANONYMOUSLY'/>
    </http>


    <authentication-manager alias='authenticationManager'>
        <authentication-provider ref='crowdAuthenticationProvider'/>
    </authentication-manager>

    <beans:bean id="crowdAuthenticationProcessingFilterEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
        <beans:property name="loginFormUrl" value="/login.html"/>
    </beans:bean>

    <beans:bean id="authenticationProcessingFilter" class="com.atlassian.crowd.integration.springsecurity.CrowdSSOAuthenticationProcessingFilter">
        <beans:property name="httpAuthenticator" ref="httpAuthenticator"/>
        <beans:property name="authenticationManager" ref="authenticationManager"/>
        <beans:property name="filterProcessesUrl" value="/j_security_check"/>
        <beans:property name="authenticationFailureHandler">
            <beans:bean class="com.atlassian.crowd.integration.springsecurity.UsernameStoringAuthenticationFailureHandler">
                <beans:property name="defaultFailureUrl" value="/console/login.action?error=true"/>
            </beans:bean>
        </beans:property>

        <beans:property name="authenticationSuccessHandler">
            <beans:bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
                <beans:property name="defaultTargetUrl" value="/console/defaultstartpage.action"/>
            </beans:bean>
        </beans:property>
    </beans:bean>

     <beans:bean id="crowdLogoutHandler" class="com.atlassian.crowd.integration.springsecurity.CrowdLogoutHandler">
        <beans:property name="httpAuthenticator" ref="httpAuthenticator"/>
    </beans:bean>

     <beans:bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
        <beans:constructor-arg value="/login.html"/>
        <beans:constructor-arg>
            <beans:list>
                <beans:ref bean="crowdLogoutHandler"/>
                <beans:bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
            </beans:list>
        </beans:constructor-arg>
        <beans:property name="filterProcessesUrl" value="/console/logoff.action"/>
    </beans:bean>

The above is the most up-to-date comprehensive example of an XML security file I can find anywhere, and it is incompatible with spring security 4.0 (if you increment the version, you get xml violations all over the place). I can't tell where/how the httpAuthenticator is getting resolved out, as I don't see any beans that have that ID, which makes it very troublesome.

Any help would be very much appreciated, I've been throwing myself at this for days now with little/no results.

Extra info:

running spring boot version 1.3.1-Release, spring security version 4.0


Solution

  • So, I think I've actually figured this out, it at least gives me a jumping-off point to further customize the spring boot integration. the primary piece of magic is the WebSecurityConfig class, which now looks like the following:

    WebSecurityConfig.java:

    package hello;
    import com.atlassian.crowd.integration.http.HttpAuthenticator;
    import com.atlassian.crowd.integration.http.HttpAuthenticatorImpl;
    import com.atlassian.crowd.integration.springsecurity.RemoteCrowdAuthenticationProvider;
    import com.atlassian.crowd.integration.springsecurity.user.CrowdUserDetailsService;
    import com.atlassian.crowd.integration.springsecurity.user.CrowdUserDetailsServiceImpl;
    import com.atlassian.crowd.service.AuthenticationManager;
    import com.atlassian.crowd.service.GroupManager;
    import com.atlassian.crowd.service.UserManager;
    import com.atlassian.crowd.service.cache.BasicCache;
    import com.atlassian.crowd.service.cache.CacheImpl;
    import com.atlassian.crowd.service.cache.CachingGroupManager;
    import com.atlassian.crowd.service.cache.CachingGroupMembershipManager;
    import com.atlassian.crowd.service.cache.CachingUserManager;
    import com.atlassian.crowd.service.cache.SimpleAuthenticationManager;
    import com.atlassian.crowd.service.soap.client.SecurityServerClient;
    import com.atlassian.crowd.service.soap.client.SecurityServerClientImpl;
    import com.atlassian.crowd.service.soap.client.SoapClientPropertiesImpl;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Properties;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    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.WebSecurityConfigurerAdapter;
    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
            .csrf().disable()
                .authorizeRequests()
                    .antMatchers("/", "/home").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")
                    .permitAll()
                    .and()
                .logout()
                    .permitAll();
        }
        public static Properties getProps() throws IOException{
         Properties prop = new Properties();
        try(InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("crowd.properties")){
            prop.load(in);
        }
        return prop;
        }
        @Bean
        public SecurityServerClient securityServerClient() throws IOException{
        return new SecurityServerClientImpl(SoapClientPropertiesImpl.newInstanceFromProperties(getProps()));
        }
       private final BasicCache cache = new CacheImpl(Thread.currentThread().getContextClassLoader().getResource("crowd-ehcache.xml"));
    
        @Bean
        public AuthenticationManager crowdAuthenticationManager() throws IOException{
    
            return new SimpleAuthenticationManager(securityServerClient());
        }
        @Bean
        public HttpAuthenticator httpAuthenticator() throws IOException{
        return new HttpAuthenticatorImpl(crowdAuthenticationManager());
        }
        @Bean
        public UserManager userManager() throws IOException{
        return new CachingUserManager(securityServerClient(), cache);
        }
        @Bean
        public GroupManager groupManager() throws IOException{
        return new CachingGroupManager(securityServerClient(), cache);
        }
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(crowdAuthenticationProvider());
        }
        @Bean
        public CrowdUserDetailsService crowdUserDetailsService() throws IOException{
        CrowdUserDetailsServiceImpl cusd = new CrowdUserDetailsServiceImpl();
        cusd.setUserManager(userManager());
        cusd.setGroupMembershipManager(new CachingGroupMembershipManager(securityServerClient(), userManager(),groupManager(),cache));
        cusd.setAuthorityPrefix("ROLE_");
        return cusd;
        }
        @Bean
        RemoteCrowdAuthenticationProvider crowdAuthenticationProvider() throws IOException{
        return new RemoteCrowdAuthenticationProvider(crowdAuthenticationManager(), httpAuthenticator(), crowdUserDetailsService());
        }
        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth
                .inMemoryAuthentication()
                    .withUser("user").password("password").roles("ROLE_USER");
        }
    }
    

    The hardest part was figuring out how to set up the beans, which was not documented at all, because before it was all XML magic. Now, assuming you have the proper crowd.properties file and the proper EhCache file setup (which could also be removed with java beans, but that's less egregious), you can use crowd integrations in a purely java fashion.

    The build.gradle to make this work, looks like the following:

    build.gradle:

    buildscript {
        repositories {
            mavenCentral()
        }
        dependencies {
            classpath("org.springframework.boot:spring-boot-gradle-plugin:1.3.1.RELEASE")
        }
    }
    if (!hasProperty('mainClass')) {
        ext.mainClass = 'hello.Application'
    }
    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'idea'
    apply plugin: 'spring-boot'
    jar {
        baseName = 'gs-securing-web'
        version =  '0.1.0'
    }
    repositories {
        maven {
        url = 'https://m2proxy.atlassian.com/repository/public'
        }
        mavenCentral()
    }
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    dependencies {
        compile "commons-codec:commons-codec:1.10"
        compile "org.springframework.boot:spring-boot-starter-web"
        compile "org.springframework.boot:spring-boot-starter"
        compile "org.springframework:spring-tx"
        compile 'org.springframework.boot:spring-boot-starter-security'
        compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
        compile (group: "com.atlassian.crowd", name: "crowd-integration-springsecurity", version: "2.8.+"){
        exclude (group: 'org.apache.ws.commons');
        }
        compile 'org.slf4j:slf4j-api'
        compile "org.codehaus.groovy:groovy"
        compile "org.codehaus.groovy:groovy-json:2.3.8"
    }
    task wrapper(type: Wrapper) {
        gradleVersion = '2.10'
    }
    

    That's it! the authentication is then properly bootstrapped to make use of crowds springsecurity interface. Now, it's worth mentioning I haven't played around with how it hands me back various Roles/etc. but this should allow me to move forward. Also, this does not include the SSO code, which I haven't totally figured out yet. So once I get that I'll post a more thorough how-to somewhere.