Custom Security Authentication
Flowable relies on the Spring Security framework to provide authentication and access control capabilities. Spring Security is an open source library that provides connectors and adapters to many security standards such as OAuth, SAML, basic authentication, etc. As with all Spring frameworks, it is designed to be highly customizable and adaptable to many security requirements.
This document describes how you can create your own Custom Authentication From Scratch and Custom Authentication With Existing Integration. If you need to create a custom identity service then have a look at Custom Identity Service. We assume that you are already familiar with the basics defined in Security and Identity Management.
Custom Authentication From Scratch
For this example we assume that you have some kind of Single Sign-On (SSO) system which performs the authorization for the users and attaches the userId, tenantId and groups as HTTP Headers on the incoming requests. Additionally, you don’t have some common libraries in your organization that integrate with Spring Security so you would need to do everything on your own.
These are the headers which are added by your SSO:
X-USER-ID: kermit
X-TENANT-ID: muppets
X-GROUP: Accounting
X-GROUP: IT
X-GROUP: HR
We now need to intercept the request and extract this information into a so called Authentication
.
Spring Security already has us covered we need to extend the AbstractPreAuthenticatedProcessingFilter
and add our extraction logic.
We will create our own implementation of the principal that would store the information.
public class CustomPrincipal implements Principal {
protected String userId;
protected String tenantId;
protected Collection<String> groups;
public CustomPrincipal(String userId, String tenantId, Collection<String> groups) {
this.userId = userId;
this.tenantId = tenantId;
this.groups = groups;
}
@Override
public String getName() {
return userId;
}
// getters omitted
}
We are now going to write the authentication filter, which would use the HttpServletRequest
to extract the information from the headers.
Extending from AbstractPreAuthenticatedProcessingFilter
allows us to easily hook in with the Spring Security authentication.
It is going to intercept all requests and perform the authentication via the AuthenticationManager
.
public class CustomHeaderAuthenticationFilter
extends AbstractPreAuthenticatedProcessingFilter {
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String userId = request.getHeader("X-USER-ID");
String tenantId = request.getHeader("X-TENANT-ID");
Enumeration<String> headersEnumeration = request.getHeaders("X-GROUP-ID");
Set<String> groups = new HashSet<>();
while (headersEnumeration.hasMoreElements()) {
groups.add(headersEnumeration.nextElement());
}
return new CustomPrincipal(userId, tenantId, groups);
}
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
// The request is already authenticated there is nothing we need to do
return "N/A";
}
}
The next step would be to write a custom AuthenticationUserDetailsService
that would load the the Spring Security UserDetails
from the PreAuthenticationToken
created by our CustomHeaderAuthenticationFilter
.
public class CustomAuthenticationUserDetailsService
implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
@Override
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) {
CustomPrincipal customPrincipal = (CustomPrincipal) token.getPrincipal();
Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
// have some custom logic to get more authorities
return User.withUsername(customPrincipal.getUserId())
.password("n/A")
.authorities(authorities)
.build();
}
}
We now have all the needed pieces to make Spring Security work with our authentication.
We are now going to add a custom SecurityScopeProvider
and SecurityScope
so that we can link our custom security with Flowable.
public class CustomSecurityScope implements SecurityScope {
protected Authentication authentication;
public CustomSecurityScope(Authentication authentication) {
this.authentication = authentication;
}
@Override
public String getUserId() {
return authentication.getName();
}
@Override
public Set<String> getGroupKeys() {
return new HashSet<>(getCustomPrincipal().getGroups());
}
@Override
public String getTenantId() {
return getCustomPrincipal().getTenantId();
}
@Override
public String getUserDefinitionKey() {
return null;
}
@Override
public boolean hasAuthority(String authority) {
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (Objects.equals(authority, grantedAuthority.getAuthority())) {
return true;
}
}
return false;
}
protected CustomPrincipal getCustomPrincipal() {
return (CustomPrincipal) authentication.getPrincipal();
}
}
public class CustomSecurityProvider implements SecurityScopeProvider {
@Override
public SecurityScope getSecurityScope(Principal principal) {
if (principal instanceof Authentication) {
return new CustomSecurityScope((Authentication) principal);
}
return new FlowablePrincipalSecurityScope(principal);
}
}
We now also have all the pieces to make the Security work with Flowable.
The final step is to actually link all of this together.
To make the linking a bit easier we are going to add a CustomHttpConfigurer
.
public class CustomHttpConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<CustomHttpConfigurer<H>, H> {
@Override
public void init(H http) {
PreAuthenticatedAuthenticationProvider authenticationProvider
= new PreAuthenticatedAuthenticationProvider();
authenticationProvider.setPreAuthenticatedUserDetailsService(
new CustomHeaderAuthenticationUserDetailsService()
); (1)
authenticationProvider = postProcess(authenticationProvider);
http
.authenticationProvider(authenticationProvider) (2)
.setSharedObject(
AuthenticationEntryPoint.class,
new Http403ForbiddenEntryPoint() (3)
);
}
@Override
public void configure(H http) {
AuthenticationManager authenticationManager
= http.getSharedObject(AuthenticationManager.class);
CustomHeaderAuthenticationFilter authenticationFilter
= new CustomHeaderAuthenticationFilter(); (4)
authenticationFilter.setAuthenticationManager(authenticationManager);
authenticationFilter = postProcess(authenticationFilter);
http.addFilter(authenticationFilter);
}
}
1 | Register our CustomHeaderAuthenticationUserDetailsService with the out of the box PreAuthenticatedAuthenticationProvider |
2 | Register the authentication provider with Spring Security |
3 | Since all our requests are authenticated via our SSO we send back HTTP 403 if there was no authentication |
4 | Create and add our CustomHeaderAuthenticationFilter |
Finally we need to register this as a Spring Configuration. For this we are going to adapt the out of the box security configuration for Flowable so our custom configuration works.
@Configuration
@Order(10)
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
protected ObjectProvider<RememberMeServices> rememberMeServicesObjectProvider;
@Autowired
protected FlowablePlatformSecurityProperties flowableSecurityProperties;
@Override
protected void configure(HttpSecurity http) throws Exception {
RememberMeServices rememberMeServices = rememberMeServicesObjectProvider.getIfAvailable();
String key = null;
if (rememberMeServices instanceof AbstractRememberMeServices) {
key = ((AbstractRememberMeServices) rememberMeServices).getKey();
}
if (flowableSecurityProperties.getRest().getCsrf().isEnabled()) {
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers((RequestMatcher) request ->
request.getHeader(HttpHeaders.AUTHORIZATION) != null);
} else {
http.csrf().disable();
}
http
// Frontend would eventually load browser's PDF viewer using object/embed
// To avoid problems in some scenarios, we need to do set X-Frame-Options sameorigin;
.headers().frameOptions().sameOrigin()
// In case you want to disable JSESSIONID cookies.
// Flowable's Authentication class expects AuthenticationSuccessEvent to be fired
// every time, and sending the cookie makes it fail from time to time. Check class
// SecurityAutoConfiguration for insights on how is this configured.
// Two things to notice:
// 1. You have to add basic auth to /frontend/engage/api/auth.ts
// Axios.defaults.auth = { username, password };
// 2. WebSockets+SocksJS, https://github.com/sockjs/sockjs-client/issues/196
//.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//.and()
.and()
.rememberMe()
.key(key)
.rememberMeServices(rememberMeServicesObjectProvider.getIfAvailable())
.and()
.apply(new CustomHttpConfigurer<>()) (1)
.and()
.authorizeRequests()
.antMatchers("/analytics-api/**").hasAuthority(SecurityConstants.ACCESS_REPORTS_METRICS)
.antMatchers("/template-api/**").hasAuthority(SecurityConstants.ACCESS_TEMPLATE_MANAGEMENT)
.antMatchers("/work-object-api/**").hasAuthority(SecurityConstants.ACCESS_WORKOBJECT_API)
// allow context root for all (it triggers the loading of the initial page)
.antMatchers("/") .permitAll()
.antMatchers(
"/**/*.svg", "/**/*.ico", "/**/*.png", "/**/*.woff2", "/**/*.css",
"/**/*.woff", "/**/*.html", "/**/*.js",
"/**/index.html").permitAll()
.anyRequest().authenticated();
}
@Bean (2)
public CustomSecurityProvider() {
return new CustomSecurityProvider();
}
}
1 | Apply our CustomConfigurer to the Spring Security configuration |
2 | Register our CustomSecurityProvider as a Spring Bean |
Custom Authentication With Existing Integration
In the previous section we assumed that your organization does not have any Spring Security Integration.
However, in case that your Organization already has some kind of integration then the only thing you need to do is do provide a custom SecurityScopeProvider
and SecurityScope
that would perform the mapping from your Organization Spring Security integration to Flowable.
For the purposes of this example lets imagine that the following stands:
-
You have a custom implementation of the Spring Security
UserDetails
namedAcmeUserDetails
. It contains thetenantId
for your user. -
Your groups are mapped as a custom
GroupGrantedAuthority
and that you have otherGrantedAuthority
(ies) which represent something else for your organization.
For this we will have the following implementations
public class AcmeSecurityScope implements SecurityScope {
protected Authentication authentication;
public AcmeSecurityScope(Authentication authentication) {
this.authentication = authentication;
}
@Override
public String getUserId() {
return authentication.getName();
}
@Override
public Set<String> getGroupKeys() {
Set<String> groupKeys = new HashSet<>();
for (GrantedAuthority authority: authentication.getGrantedAuthorities()) {
if (authority instanceof GroupGrantedAuthority) {
groupKeys.add(((GroupGrantedAuthority) authority).getGroupId());
}
}
return groupKeys;
}
@Override
public String getTenantId() {
return getAcmeUserDetails().getTenantId();
}
@Override
public String getUserDefinitionKey() {
// If you need you can do a mapping between certain attributes from your
// user details to a Flowable User Definition
return null;
}
@Override
public boolean hasAuthority(String authority) {
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (Objects.equals(authority, grantedAuthority.getAuthority())) {
return true;
}
}
return false;
}
protected AcmeUserDetails getAcmeUserDetails() {
return (AcmeUserDetails) authentication.getDetails();
}
}
public class AcmeSecurityProvider implements SecurityScopeProvider {
@Override
public SecurityScope getSecurityScope(Principal principal) {
if (principal instanceof Authentication) {
return new AcmeSecurityScope((Authentication) principal);
}
return new FlowablePrincipalSecurityScope(principal);
}
}
Finally we need to register this as a Spring Configuration.
You would need to consult with your organization documentation for how to configure your security configuration.
The only thing needed for the integration with Flowable to work is to have the AcmeSecurityScopeProvider
registered as a Spring Bean
@Configuration
public class AcmeFlowableConfiguration {
@Bean
public AcmeSecurityProvider() {
return new AcmeSecurityProvider();
}
}