Spring Boot-Embedded Camunda Single Sign-on With SAML IDP Provider
From flow chart to implementation.
Join the DZone community and get the full member experience.
Join For FreeSingle sign-on flow:
- User tries to access Camunda web apps
- Camunda apps detect that the user has not logged into IDP and creates SAML request redirects the user to IDP
- The user enters credentials in IDP. After successful login, IDP sends a SAML response to Camunda
- The app parses response and sets into the spring security context and passes the control to the Camunda Authentication filter
- The Camunda custom authentication provider takes authenticated user details, such as name and group, and passes them on to the Camunda authorization service
- The Camunda authorization service, based on user roles, opens a welcome page
Implementation
To achieve SSO, we should use Spring Security so that Spring framework handles the authentication and passes the authenticated user on to Camunda.
We only need to add the ContainerBasedAuthenticationFilter that ships with the Camunda product and provide a custom authentication provider.
By implementing a class that implements the org.camunda.bpm.engine.rest.security.auth.AuthenticationProvider interface, one should be able to provide authentication details.
I have used Onelogin as the IDP provider, created a trial account in https://www.onelogin.com/free-trial, and set up the application. Then, I downloaded metadata.xml and placed the metadata.xml file inside the resource directory.
Application.yaml file:
xxxxxxxxxx
onelogin
metadata-path classpath onelogin_metadata_1268214.xml
sp
protocol http
host localhost8080
path /
key-store
file classpath keystore.jks
alias onelogin
password secret
spring.datasource.url jdbc h2 file ./camunda-h2-database4
spring.h2.console.enabledtrue
camunda.bpm
filter
create All tasks
camunda
bpm
authorization
enabledtrue
admin-user
id admin
password admin
Spring security Implementation to connect to IDP:
package com.prad.samldemo.config;
import static org.springframework.security.extensions.saml2.config.SAMLConfigurer.saml;
import java.util.Collections;
import javax.servlet.Filter;
import org.camunda.bpm.engine.rest.security.auth.ProcessEngineAuthenticationFilter;
import org.camunda.bpm.webapp.impl.security.auth.ContainerBasedAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
("${onelogin.metadata-path}")
private String metadataPath;
("${onelogin.sp.protocol}")
private String spProtocol;
("${onelogin.sp.host}")
private String spHost;
("${onelogin.sp.path}")
private String spBashPath;
("${onelogin.sp.key-store.file}")
private String keyStoreFile;
("${onelogin.sp.key-store.password}")
private String keyStorePassword;
("${onelogin.sp.key-store.alias}")
private String keyStoreAlias;
("${onelogin.sp.protocol}")
private String protocol;
private SAMLUserService samlUserService;
protected void configure(final HttpSecurity http) throws Exception {
// @formatter:off
http.csrf().disable();
http.authorizeRequests().antMatchers("/saml/**").permitAll().anyRequest().authenticated().and().apply(saml())
.userDetailsService(samlUserService).serviceProvider().protocol(spProtocol).hostname(spHost)
.basePath(spBashPath).keyStore().storeFilePath(keyStoreFile).keyPassword(keyStorePassword)
.keyname(keyStoreAlias).and().and().identityProvider().metadataFilePath(metadataPath).and().and();
// @formatter:on
}
public FilterRegistrationBean containerBasedAuthenticationFilter() {
System.out.println("Inside Spring Filter");
FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
filterRegistration.setFilter(new ContainerBasedAuthenticationFilter());
filterRegistration.setInitParameters(Collections.singletonMap("authentication-provider",
"com.prad.samldemo.config.SpringSecurityAuthenticationProvider"));
filterRegistration.setOrder(101); // make sure the filter is registered after the Spring Security Filter Chain
filterRegistration.addUrlPatterns("/camunda/*");
return filterRegistration;
}
public FilterRegistrationBean<Filter> processEngineAuthenticationFilter() {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
registration.setName("camunda-auth");
registration.setFilter(getProcessEngineAuthenticationFilter());
registration.setInitParameters(Collections.singletonMap("authentication-provider",
"com.prad.samldemo.config.SpringSecurityAuthenticationProvider"));
registration.addUrlPatterns("/engine-rest/*");
return registration;
}
public Filter getProcessEngineAuthenticationFilter() {
return new ProcessEngineAuthenticationFilter();
}
}
Container-based authentication filter implementation:
xxxxxxxxxx
package com.prad.samldemo.config;
import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.rest.security.auth.AuthenticationResult;
import org.camunda.bpm.engine.rest.security.auth.impl.ContainerBasedAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
import java.util.List;
import java.util.stream.Collectors;
public class SpringSecurityAuthenticationProvider extends ContainerBasedAuthenticationProvider {
public AuthenticationResult extractAuthenticatedUser(HttpServletRequest request, ProcessEngine engine) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return AuthenticationResult.unsuccessful();
}
String name = ((User)authentication.getPrincipal()).getId();
List<String> roles = ((User)authentication.getPrincipal()).getRoles();
if (name == null || name.isEmpty()) {
return AuthenticationResult.unsuccessful();
}
AuthenticationResult authenticationResult = new AuthenticationResult(name, true);
// authenticationResult.setGroups(getUserGroups(authentication));
authenticationResult.setGroups(roles);
return authenticationResult;
}
private List<String> getUserGroups(Authentication authentication){
List<String> groupIds;
groupIds = authentication.getAuthorities().stream()
.map(res -> res.getAuthority())
.map(res -> res.substring(5)) // Strip "ROLE_"
.collect(Collectors.toList());
return groupIds;
}
}
SAML user service:
xxxxxxxxxx
package com.prad.samldemo.config;
import java.util.Arrays;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.security.saml.userdetails.SAMLUserDetailsService;
import org.springframework.stereotype.Service;
public class SAMLUserService implements SAMLUserDetailsService {
public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException {
String[] array = credential.getAttributeAsStringArray("roles");
return new User(credential.getNameID().getValue() , Arrays.asList(array));
}
}
User model:
xxxxxxxxxx
package com.prad.samldemo.config;
import java.util.List;
public class User {
private String id;
private List<String> roles;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public User(String id, List<String> roles) {
this.id = id;
this.roles = roles;
}
}
Opinions expressed by DZone contributors are their own.
Comments