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:
- Eclipse EE 4.7.0 (with Maven 3.3.9)
- Spring Boot 1.5.4.RELEASE
- Spring Security 4.2.3.RELEASE
- Using Crowd (Server) 3.0.1
- crowd-integration-springsecurity 3.0.1
The guide is broken down (loosely) into the following:
- getting all required Crowd libraries and files integrated;
- wiring all beans (outline both xml or Java bean methods);
- 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 .
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):
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:
- I've neglected to create a bean for
CrowdSecurityFilter
that existed in applicationContext-CrowdRestClient.xml
; - 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
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
- Main guide (didn't quite work for me): https://confluence.atlassian.com/crowd/integrating-crowd-with-spring-security-174752019.html
- Atlassian maven integration https://developer.atlassian.com/server/crowd/maven-2-integration/
- Atlassian's maven repo: https://m2proxy.atlassian.com/content/groups/public/
- https://community.atlassian.com/t5/Answers-Developer-Questions/Spring-Security-Crowd-REST-API/qaq-p/571924
- https://community.atlassian.com/t5/Answers-Developer-Questions/Crowd-and-Spring-Security-4-integration-with-SSO/qaq-p/526057
- https://community.atlassian.com/t5/Answers-Developer-Questions/Crowd-Integration-with-Spring-boot-Spring-security-using-Java/qaq-p/510765
- http://www.baeldung.com/spring-security-custom-filter
Related articles
-
Page:
-
Page:
-
Page:
-
Page:
-
Page: