What do you think? Discuss, post comments, or ask questions at the end of this article [More about me]

So, recently I managed to successfully integrate one of my Spring Boot web applications with Atlassian Crowd.  I had set aside a day for this, thinking that surely, Atlassian would have some nice guides and examples available.  Turns out their guide (here) is a bit out of date and a tad incomplete.

Getting everything working properly ended up taking much longer than what I set aside.  This was mainly because of the lack of good documentation and examples.  I spent a lot of time searching for posts and bits of information that really should be in Atlassian's guide - I've added some of the very helpful links to information that proved invaluable in properly integrating Crowd with my Spring Boot app.

NOTE: I do love Atlassian and their application stack (especially Crowd)... but dang, hopefully someone will update the supporting documentation for integrating Crowd with Spring (and other environments).  Let me know if you need a hand with this as I'd be happy to contribute to help other devs avoid the issues I ran into.

Guide

This guide is intended to help other poor (developer) souls that have struggled with integrating Crowd with Spring Security.  It was done with the following versions:

  1. Eclipse EE 4.7.0 (with Maven 3.3.9)
  2. Spring Boot 1.5.4.RELEASE
  3. Spring Security 4.2.3.RELEASE
  4. Using Crowd (Server) 3.0.1
  5. crowd-integration-springsecurity 3.0.1

The guide is broken down (loosely) into the following:

  1. getting all required Crowd libraries and files integrated;
  2. wiring all beans (outline both xml or Java bean methods);
  3. Configuration of SecurityConfig.class; and stopping CrowdSecurityFilter from redirecting everything to crowd.properties login.url (we want Spring to take care of redirection and Crowd only for authentication and providing Authorities etc.);

Integrating all Crowd required libraries and files with spring-security

Preparing Crowd access for our Spring Boot app

So, first up we need to prepare Crowd to allow access for our application in development.  I won't cover this here as it's a very standard (and non-confusing) process.  This is pretty much the only part of Atlassian's guide that was straight-forward (sad)

See Step 1 of the Atlassian guide for this.

Maven, maven, maven... it's wasn't your fault

Now is where things go a bit awry.  Initially I tried (and failed) in both methods outlined in the guide (using maven, or manually copying .jars and adding to build path).  I'd prefer using Maven for dependency management so will focus on that approach... turns out...

Atlassian forgot to mention you need to add their maven repository to maven first... nowhere was this outlined or mentioned in their guide.

So, first add the following to the repositories section of your pom.xml, (as outlined here):

	<repository>
        <id>central</id>
        <url>https://m2proxy.atlassian.com/repository/public</url>
        <snapshots>
            <enabled>true</enabled>
            <updatePolicy>always</updatePolicy>
        </snapshots>
        <releases>
            <enabled>true</enabled>
        </releases>
    </repository>

Yay, so once that's done, simply (I hate that word...) add the following dependency to the dependencies section of your pom.xml:

	<dependency>
        <groupId>com.atlassian.crowd</groupId>
        <artifactId>crowd-integration-springsecurity</artifactId>
        <version>3.0.1</version>
    </dependency>

Note1: Atlassian's guide gets all fancy by adding a property to reference the version.  Since this is the only artifact we need, we'll just add the version number (3.0.1) directly to the dependency instead.

Note2: the guide also says to make the dependency scope runtime.  We're not going to do that as we're going to wire our beans via Java and not use applicationContext-CrowdRestClient.xml as the guide outlines (you'll see why later).

Once added, run a mvn clean install (however you run maven goals) and it should compile without maven dependency errors (i.e. it should have got the required crowd libraries from the maven repo).

Copying required files

As per the Atlassian guide, I did need to copy the following files into your application's classpath:

Copy From

Copy To

CROWD/client/conf/crowd-ehcache.xml

SpringSecApp/WEB-INF/classes/crowd-ehcache.xml

CROWD/client/conf/crowd.properties

SpringSecApp/WEB-INF/classes

With the modified values in the crowd.properties file relevant to my Crowd server.

Wiring beans

Now that we've got the required jars (via maven) and necessary files we're ready to wire spring beans and actually integrate our application with Crowd.

There's several ways of doing this, the first is outlined in Step 3 of Atlassian's guide.  This approach is pretty easy, but we are actually going to wire our beans in Java (read on for the reasons why).

Creating spring beans in SecurityConfig.java

We're pretty much going to port over nearly all the bean definitions from applicationContext-CrowdRestClient.xml to our SecurityConfig class.

Below you'll see my SecurityConfig.class in it's entirety.  Look at the @Bean definitions and you should be able to see the porting pattern from applicationContext-CrowdRestClient.xml (that's if you're interested in understanding that... otherwise you can copy most of the code below):

SecurityConfigCrowd.java
package com.adfaspace.jay.app.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
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;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.session.HttpSessionEventPublisher;

import com.adfaspace.jay.app.Application;
import com.adfaspace.jay.backend.data.LocalAuthority;
import com.adfaspace.jay.backend.service.UserService;
import com.atlassian.crowd.integration.http.CrowdHttpAuthenticatorImpl;
import com.atlassian.crowd.integration.http.util.CrowdHttpTokenHelper;
import com.atlassian.crowd.integration.http.util.CrowdHttpTokenHelperImpl;
import com.atlassian.crowd.integration.http.util.CrowdHttpValidationFactorExtractor;
import com.atlassian.crowd.integration.http.util.CrowdHttpValidationFactorExtractorImpl;
import com.atlassian.crowd.integration.rest.service.factory.RestCrowdClientFactory;
import com.atlassian.crowd.integration.springsecurity.RemoteCrowdAuthenticationProvider;
import com.atlassian.crowd.integration.springsecurity.user.CrowdUserDetailsServiceImpl;
import com.atlassian.crowd.service.client.ClientPropertiesImpl;
import com.atlassian.crowd.service.client.ClientResourceLocator;
import com.atlassian.crowd.service.client.CrowdClient;

@EnableWebSecurity
@Configuration
public class SecurityConfigCrowd extends WebSecurityConfigurerAdapter {
	
	private final RedirectAuthenticationSuccessHandler successHandler;
	
	@Autowired
	private UserService userService;
	
	/**
	 * Default constructor
	 */
	@Autowired
	public SecurityConfigCrowd(RedirectAuthenticationSuccessHandler successHandler) {
		this.successHandler = successHandler;
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.authenticationProvider(crowdAuthenticationProvider());
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// Not using Spring CSRF here to be able to use plain HTML
		http.csrf().disable();
		
		ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry reg = http
				.authorizeRequests();

		// Allow access to static resources
		reg = reg.antMatchers("/static/**").permitAll();
		// Require authentication for all URLS ("/**")
		reg = reg.antMatchers("/**").authenticated();
		HttpSecurity sec = reg.and();

		// Allow access to login page without login
		FormLoginConfigurer<HttpSecurity> login = sec.formLogin().permitAll();
		login = login.loginPage(Application.LOGIN_URL).loginProcessingUrl(Application.LOGIN_PROCESSING_URL)
				.failureUrl(Application.LOGIN_FAILURE_URL).successHandler(successHandler);
		login.and().logout().logoutSuccessUrl(Application.LOGOUT_URL);
		
		// session management
		http.sessionManagement().maximumSessions(3).sessionRegistry(sessionRegistry());
	}
	
	@Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
        return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
    }
    
    @Bean
    public ClientResourceLocator resourceLocator() {
        return new ClientResourceLocator("crowd.properties");
    }
    
    @Bean
    public ClientPropertiesImpl clientProperties() {
    	return ClientPropertiesImpl.newInstanceFromResourceLocator(resourceLocator());
    }
    
    @Bean
    public RestCrowdClientFactory crowdClientFactory() {
    	return new RestCrowdClientFactory();
    }
    
    @Bean
    public CrowdClient crowdClient() {
    	return crowdClientFactory().newInstance(clientProperties());
    }
    
    @Bean
    public CrowdHttpValidationFactorExtractor validationFactorExtractor() {
    	return CrowdHttpValidationFactorExtractorImpl.getInstance();
    }
    
    @Bean
    public CrowdHttpTokenHelper tokenHelper() {
    	return CrowdHttpTokenHelperImpl.getInstance(validationFactorExtractor());
    }
    
    @Bean
    public CrowdHttpAuthenticatorImpl crowdHttpAuthenticator() {
    	return new CrowdHttpAuthenticatorImpl(crowdClient(), clientProperties(), tokenHelper());
    }
    
    @Bean
    public CrowdUserDetailsServiceImpl crowdUserDetailsService() {
    	
    	 CrowdUserDetailsServiceImpl cuds = new CrowdUserDetailsServiceImpl();
    	 cuds.setCrowdClient(crowdClient());
    	 return cuds;
    }
    
	@Bean
	public RemoteCrowdAuthenticationProvider crowdAuthenticationProvider() {
		
		return new CrowdAuthenticationProviderExt(crowdClient(), crowdHttpAuthenticator(), crowdUserDetailsService(),
				userService);
	}
}

You might notice that there's couple of interesting things in my SecurityConfig.java class:

  1. I've neglected to create a bean for CrowdSecurityFilter that existed in applicationContext-CrowdRestClient.xml;
  2. I've extended the standard CrowdAuthenticationProvider class with my CrowdAuthenticationProviderExt.java class.

The reason why I did NOT create a bean definition for CrowdSecurityFilter is that the standard CrowdSecurityFilter causes all traffic to be redirected to the application.login.url in your crowd.properties file - even traffic to my spring apps login page.  Putting the application.login.url property to my spring application login page causes a 'too many redirects' error.  That... kind of sucks as I want my Spring app to allow users to login and also to take care of redirecting to the login page (and not crowd).

I've extended the RemoteCrowdAuthenticationProvider class to implement my own methods for creating local User objects that I persist in my applications persistency layer (HIbernate).  That is, when a valid Crowd user logs into my application, I create a User (from my application's perspective) and save that. 

For completeness, see my extended class below

CrowdAuthenticationProviderExt
package com.adfaspace.jay.app.security;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

import com.adfaspace.jay.backend.service.UserService;
import com.atlassian.crowd.embedded.api.User;
import com.atlassian.crowd.exception.ApplicationPermissionException;
import com.atlassian.crowd.exception.ExpiredCredentialException;
import com.atlassian.crowd.exception.InactiveAccountException;
import com.atlassian.crowd.exception.InvalidAuthenticationException;
import com.atlassian.crowd.exception.OperationFailedException;
import com.atlassian.crowd.exception.UserNotFoundException;
import com.atlassian.crowd.integration.http.CrowdHttpAuthenticator;
import com.atlassian.crowd.integration.springsecurity.RemoteCrowdAuthenticationProvider;
import com.atlassian.crowd.integration.springsecurity.user.CrowdUserDetailsService;
import com.atlassian.crowd.service.client.CrowdClient;

public class CrowdAuthenticationProviderExt extends RemoteCrowdAuthenticationProvider{
	
	private final UserService userService;
	
	public CrowdAuthenticationProviderExt(CrowdClient authenticationManager, CrowdHttpAuthenticator httpAuthenticator,
			CrowdUserDetailsService userDetailsService, UserService userService) {
		
		super(authenticationManager, httpAuthenticator, userDetailsService);
		
		this.userService = userService;
	}
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		
		String userName = authentication.getName();
		
		User crowdUser = null;
		
		try {
			crowdUser = authenticationManager.authenticateUser(userName, (String) authentication.getCredentials());
			
		} catch (Exception e) {
			
			System.out.println(e.getMessage());
			return null;
		}
		
		// init
		com.adfaspace.jay.backend.data.entity.User user = null;
		
		// check if have a persistent local user copy
		if (!userService.userWithNameExists(userName)) {
			// create local user for this crowd user
			user = userService.createUserFromCrowdUser(userName, false);
		} else {
			user = userService.findByName(userName, null);
		}
		
		// update transient-like properties and save user to user repo
		user = setTransientPropertiesAndSaveUser(crowdUser, user);
		
		// create token for spring
		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user,
				authentication.getCredentials(), userService.getAuthorities(user.getName()));
		
		return token;
	}
	
	/**
	 * Sets properties on local users that should be updated everytime a crowdUser logs in.
	 * @param crowdUser
	 * @param user
	 */
	private com.adfaspace.jay.backend.data.entity.User setTransientPropertiesAndSaveUser(User crowdUser, com.adfaspace.jay.backend.data.entity.User user) {
		
		user.setActive(crowdUser.isActive());
		user.setDirectoryId(crowdUser.getDirectoryId());
		
		// save changes
		return userService.save(user);
	}
}

Summary

The above should get you going in integrating Crowd with your Spring security application.

I must admit, writing this up doesn't do the amount of searching and banging my head against my computer desk (figuratively speaking of course) I had to do to get everything working.  The above combination of things worked... but I tried pretty much every other approach before refining and simplifying it to the above. 

In any case, glad I got it working and best of luck to other developers who may be struggling with similar issues.

Please see below for references to posts and other content that were instrumental in figuring this out.

References

  1. Main guide (didn't quite work for me): https://confluence.atlassian.com/crowd/integrating-crowd-with-spring-security-174752019.html
  2. Atlassian maven integration https://developer.atlassian.com/server/crowd/maven-2-integration/
  3. Atlassian's maven repo: https://m2proxy.atlassian.com/content/groups/public/
  4. https://community.atlassian.com/t5/Answers-Developer-Questions/Spring-Security-Crowd-REST-API/qaq-p/571924
  5. https://community.atlassian.com/t5/Answers-Developer-Questions/Crowd-and-Spring-Security-4-integration-with-SSO/qaq-p/526057
  6. https://community.atlassian.com/t5/Answers-Developer-Questions/Crowd-Integration-with-Spring-boot-Spring-security-using-Java/qaq-p/510765
  7. http://www.baeldung.com/spring-security-custom-filter