Skip to main content

Security

Security and Identity Management

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.

Authentication and Authorization

The out of the box security configuration for Flowable provides a simple configuration that can be extended and adapted according to your needs.

Upon an authentication attempt, the user is loaded through the FlowablePlatformUserDetailsService (which is called from DaoAuthenticationProvider as this class delegates to an instance of the UserDetailsService) and given that the user exists, an instance of org.springframework.security.core.userdetails.UserDetails is created. The UserDetails object contains the following information:

  • The username and password of the user

  • Granted authorities for the user:

    • Group keys prefixed with GROUP_

    • Tenant id prefixed with TENANT_ (only needed for multi-tenant applications)

    • User definition key prefixed with USER_DEFINITION_KEY_

    • Authorities retrieved via the com.flowable.core.spring.security.UserDefinitionAuthoritiesProvider

After the Spring Security filter chain is executed, the Spring Security Context contains the authenticated user information, which is used for access control or other purposes.

820 architecture

Flowable uses the com.flowable.core.spring.security.SecurityUtils to retrieve all the needed information for the Flowable Authorization. SecurityScope Every action uses com.flowable.core.common.api.security.SecurityScope to get the required information.

  • SecurityScope#getUserId() provides the id of the current user.

  • SecurityScope#getTenantId() provides the tenant id of the current user.

  • SecurityScope#getGroupKeys() provides the group keys of the current user.

  • SecurityScope#getUserDefinitionKey() provides the user definition key of the current user.

  • SecurityScope#hasAuthority(String) provides a way to check if the current user has the given authority.

The SecurityScope is provided via a bean com.flowable.core.common.api.security.SecurityScopeProvider. The default implementation uses the org.springframework.security.core.Authentication to get access to the relevant information.

  • SecurityScope#getUserId() retrieved via Authentication#getName().

  • SecurityScope#getTenantId() the first GrantedAuthority that is prefixed with TENANT_.

  • SecurityScope#getGroupKeys() all granted GrantedAuthority(ies) that have the GROUP_ prefix

  • SecurityScope#getUserDefinitionKey() the first GrantedAuthority that is prefixed with USER_DEFINITION_KEY_

  • SecurityScope#hasAuthority(String) provides a way to check if the current user has the given authority.

For example, if the user has the following GrantedAuthority(ies)

  • GROUP_user

  • GROUP_admin

  • TENANT_acme

  • access-reports

The current user group keys are user and admin, and the current user tenant id is acme.

Administrators

When doing calls over REST Flowable needs to know whether the user doing the request is an admin user or not. The reason for this is that admin users are allowed to access all data (within their tenant). For these users it is not checked if they have access to the data via the IdentityLink(s). Such users are the technical users used by Flowable Control to get data for a particular Flowable instance.

By default, a user is an admin user if they are a member of the flowableAdministrator group. However, it is possible to provide a custom logic for determining whether a user is an admin or not. This can be done by implementing the com.flowable.core.spring.security.interceptor.PlatformSecurityInterceptor. If such bean exists, then PlatformSecurityInterceptor#hasAdminRights(Authentication) would be called to determine whether the current user Authentication has admin rights or not. Furthermore, you can also set a comma-separated list of users by setting the userId of the users which should be admin also through the property flowable.platform.idm.additional-admin-users.

This means that you are in full control to decide how you want to determine what an admin user is.

Super Administrators

Super administrators are admin users which are in the empty or default tenant. These users have access to the data across all tenants in the system. Such users are the technical users used by Flowable Control to get data for a particular Flowable instance.

Customizing the Default Configuration

As other Flowable engines, the Identity Management (IDM) engine provides extension points to override default beans by custom ones. For instance, if in your project, you provide a custom UserDetailsService, the security auto-configuration (com.flowable.autoconfigure.security.FlowablePlatformSecurityAutoConfiguration) finds the bean and does not use the default bean.

See the following classes for more configuration options:

  • com.flowable.spring.boot.FlowableSecurityAutoConfiguration

  • com.flowable.autoconfigure.security.FlowablePlatformSecurityAutoConfiguration

  • com.flowable.core.spring.security.FlowablePlatformUserDetailsService

  • com.flowable.autoconfigure.security.FlowablePlatformSecurityProperties

In order for Flowable to be able to use the correct userId, group keys, tenant id and user definition key then a custom bean implementation of com.flowable.core.common.api.security.SecurityScopeProvider should be provided.

Have a look at Custom Security Authentication for more details about how to customize the authentication and Custom Identity Service for more details how to write a custom PlatformIdentityService.

Identity Management Introduction

Most of the BPMN and CMMN models in Flowable involve interactions with users, for instance, creating tasks, assigning cases, etc. In the case of Flowable with chat capabilities, the users can also be part of conversations (chats) with other users. The Flowable IDM engine encapsulates and provides all user and group-related features and services. Queries for users and groups can be defined in PlatformIdentityService, which delegates the execution to the corresponding Entity Managers, which finally query the database for the information.

Besides the normal querying functions (methods createPlatformUserQuery and createPlatformGroupQuery), this service provides other user management capabilities:

  • User: create, update, deactivate, change password, set presence information etc.

  • Group: create, update, manage memberships, etc.

  • Privileges: create, delete, assign to users and groups, etc.

Overriding the Default Identity Service

The default PlatformIdentityService is sufficient if the users are synced in the Flowable Database. However, in most cases you have your own identity store (LDAP, Azure AD, or something else). In this case you would need to provide your own implementation of the PlatformIdentityService.

The API has certain methods that do not necessarily need a custom implementation and can use the default implementation. Ideally would be to subclass ReadOnlyIdmIdentityService and implement the following methods:

  • Retrieve methods - These are the most important methods and are used by Flowable to query users for assigning tasks

  • Create, Update and Delete methods - In case you want to support user / group creation you can implement the following methods:

    • Group newGroup(String)

    • void saveGroup(Group)

    • void deleteGroup(String)

    • PlatformGroupBuilder createNewGroupBuilder(String)

    • PlatformGroupBuilder createUpdateGroupBuilder(String)

    • User newUser(String)

    • void saveUser(User)

    • void deleteUser(String)

    • PlatformUserBuilder createNewUserBuilder(String)

    • PlatformUserBuilder createUpdateUserBuilder(String)

    • void createMembership(String, String)

    • void deleteMembership(String, String)

  • Password related methods

    • void checkPassword(String, String)

    • void updateUserPassword(User)

  • Native query methods - These are not used by the Platform, you can throw an exception if you don’t need to support it

    • NativeUserQuery createNativeUserQuery()

    • NativeGroupQuery createNativeGroupQuery()

  • Privilege methods - These are used by the IDM REST API to retrieve the users and groups for a certain privilege. See Privileges for more info.

    • List<Group> getGroupsWithPrivilege(String)

    • List<User> getUsersWithPrivilege(String)

  • Deprecated methods scheduled for removal

    • PlatformUser findPlatformUserById(String) - You can redirect it to createPlatformUserQuery().userId(String)

    • PlatformGroup findPlatformGroupById(String) - You can redirect it to createPlatformGroupQuery().groupId(String)

    • List<PlatformGroup> findPlatformGroupsForUser(String) - You can redirect it to createPlatformGroupQuery().groupMember(String)

In order for your custom PlatformIdentityService to be configured with the CoreIdmEngineConfiguration, you need to use an EngineConfigurationConfigurer as follows:

@Bean
public EngineConfigurationConfigurer<CoreIdmEngineConfiguration> customCoreIdmEngineConfigurer() {
return idmEngineConfiguration -> {
idmEngineConfiguration.setIdmIdentityService(new YourCustomIdentityService());
};
}
info

Do not define your custom identity service as a bean. The PlatformIdentityService is exposed as a bean once the Core IDM engine is created.

For more details about writing your own PlatformIdentityService have a look at Custom Identity Service.

Multi-tenancy

In the Flowable default implementation of the PlatformIdentityService a single user can only be a member of a single tenant. However, we are not forcing this requirement on the PlatformIdentityService. Therefore, in all the relevant places where the PlatformUserQuery and PlatformGroupQuery are used in the Platform we are passing the tenant id of the current user or current process / case.

The findUser(userId) function, that can be used in processes or cases, has been adapted to pass the process / case in which it is invoked. This means that when querying for that user we are going to pass the tenant id of the process / case to the PlatformUserQuery.

When querying for users or groups over the REST API we are always going to pass the current user tenant id in the requests. The only exception to this rule is when a super admin does the querying.

See Multi-tenancy for more information on multi-tenancy.

Privileges

If you have browsed through some of the classes mentioned above, you might have noticed the Privilege entities. These entities provide a custom extension point to add special permissions to users and groups. Technically, they are mapped to org.springframework.security.core.GrantedAuthority, which means they can be used to restrict access to specific endpoints; for example:

.antMatchers(“/service/reports/*”).hasAuthority(“reports”)

Remember Me Services

Flowable provides stateless applications: no information is stored in any of the servers running them. However, sometimes, it is important to store some data to increase the end-user happiness. A good example is user tokens, which are stored by the RememberMe Services in the database so that users do not have to log in again in the case a server is down. The default RememberMe Services of Flowable enrich the tokens with useful information, such as User-Agent and IP addresses, enabling further possibilities for special auditing use cases.

LDAP

This section will explain how to configure your custom Flowable application to work with LDAP. For all the configuration properties have a look at the LDAP Installation Guide.

In order to work with LDAP in your application you need to add the following dependencies (on top of the default flowable ones):

<dependency>
<groupId>com.flowable.core</groupId>
<artifactId>flowable-spring-boot-starter-ldap</artifactId>
</dependency>

<!-- LDAP Security needed only if projects need to use the LDAP Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>

Then in your SecurityConfiguration you'll need to do the following:

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class SecurityConfiguration {

@Bean
public FlowableLdapAuthenticationProviderFactory ldapAuthenticationProvider() {
return new FlowableLdapAuthenticationProviderFactory();
}

@Bean
@Order(10)
public SecurityFilterChain ldapDefaultSecurity(HttpSecurity http, ObjectProvider<FlowableHttpSecurityCustomizer> httpSecurityCustomizers) throws Exception {
for (FlowableHttpSecurityCustomizer customizer : httpSecurityCustomizers.orderedStream()
.collect(Collectors.toList())) {
customizer.customize(http);
}
// ...

return http.build();
}
}

Using the FlowableLdapAuthenticationProviderFactory will make sure that the configuration properties are properly applied for authentication security. If you need to customize something, you use the FlowableLdapAuthenticationProviderFactory and customize it.

OAuth2

This section will explain how to configure your custom Flowable application to work with OAuth2. For all the configuration properties have a look at the OAuth2 Installation Guide.

Depending on how you want to use OAuth2 for your application you will need to add some or all of the following dependencies (on top of the default flowable ones):

  • spring-boot-starter-oauth2-client - When the users will log in to the application by being redirected to the SSO.
  • spring-boot-starter-oauth2-resource-server - When the application is accessed using already authenticated users / machines. i.e. machine to machine communication, Flowable Control to Work, Flowable Design to Work, etc.
 <!-- OAuth2 Client needed only if projects needs to use OAuth2 Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<!--
OAuth2 Resource Server needed only if projects need to use OAuth2 Security to communicate with the Flowable REST API.
e.g. machine to machine, Flowable Control to Work, Flowable Design to Work
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Then in your SecurityConfiguration you'll need to do the following:

import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;

import jakarta.servlet.DispatcherType;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatchers;

import com.flowable.autoconfigure.security.FlowableHttpSecurityCustomizer;
import com.flowable.core.spring.security.token.FlowableJwtResourceServerConfigurer;
import com.flowable.core.spring.security.web.savedrequest.MatchingRequestParameterNameRemovalRedirectFilter;

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class SecurityConfiguration {

@Bean
@Order(10)
public SecurityFilterChain oauthDefaultSecurity(HttpSecurity http, ObjectProvider<FlowableHttpSecurityCustomizer> httpSecurityCustomizers) throws Exception {
for (FlowableHttpSecurityCustomizer customizer : httpSecurityCustomizers.orderedStream().toList()) {
customizer.customize(http);
}

http.exceptionHandling(exceptionHandling -> exceptionHandling
// Using this entry point as a default entry point if none of the others match (the first default entry point is the default entry point).
// This one will not match any request, so it won't override other defaults from Spring Security
.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), RequestMatchers.not(AnyRequestMatcher.INSTANCE))
// using this no op authentication entry point, due to Spring Boot (3.x) no longer using a default error handling
.defaultAuthenticationEntryPointFor((request, response, authException) -> {}, new DispatcherTypeRequestMatcher(DispatcherType.ERROR))
.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"))
);

// We mark all requests as authenticated in order for the redirect to happen when the application is accessed
// We allow the favicon to always be available, since it is invoked by the browser itself
http.authorizeHttpRequests(configurer -> configurer
.requestMatchers(antMatcher("/favicon.ico")).permitAll()
.anyRequest().authenticated()
);
// Currently an HttpSessionSecurityContextRepository is needed for the oauth2 to work
HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
securityContextRepository.setDisableUrlRewriting(true);
http.securityContext(securityContext -> securityContext.securityContextRepository(securityContextRepository));

http.addFilterAfter(new MatchingRequestParameterNameRemovalRedirectFilter(), RequestCacheAwareFilter.class);

http.oauth2Login(Customizer.withDefaults());
http.oauth2Client(Customizer.withDefaults());

ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
JwtDecoder jwtDecoder = applicationContext
.getBeanProvider(JwtDecoder.class)
.getIfAvailable();
if (jwtDecoder != null) {
// Doing this since Flowable Access token authentication could be enabled through properties.
// if you need to use Bearer authentication, and Flowable Access token then you should register the jwt configurer only if the flowable one isn't already configured
if (http.getConfigurer(FlowableJwtResourceServerConfigurer.class) == null) {
http.oauth2ResourceServer(configurer -> configurer.jwt(Customizer.withDefaults()));
}
}

return http.build();
}
}

Using the FlowableHttpSecurityCustomizer will make sure that the configuration properties are applied for authentication security. The Flowable Security auto configurations will make sure that the required mapping properties are applied.

Flowable Access Tokens

v3.15.0+

Since 3.15 Flowable Work has an option to create access tokens for users. This access tokens can be used with any other custom authentication mechanism. In order to enable them you'll need to set the flowable.platform.idm.token-signing-secret with a value with at least 32 characters.

note

If you change the property then the tokens that were created prior to the change will no longer be valid.

Once the property is set all user that have a user definition that has the personalAccessTokens allowed feature are going to be able to create their own access tokens.

Creating a token can be done by going to settings by clicking on the user in the bottom left of the screen and clicking on settings

Access Tokens Settings

The Access tokens are available in the Access Tokens tab. The "New token" button can be used to start creating a new token

Access Tokens View

When creating a token you need to give it a name and choose for how long you want it to be valid.

Access Tokens Create

Once you create the token you will see its value. This is the only time when you can see the value, you can copy it and use it.

Access Tokens Value

Once you create the token you will be able to see it in the Access Tokens tab. Here you can also manage your tokens, i.e. revoke them if they need to be revoked.

Access Tokens View Available Tokens

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:

Incoming Headers.

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.

CustomPrincipal.

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 uses 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.

CustomHeaderAuthenticationFilter.

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 is to write a custom AuthenticationUserDetailsService that loads the Spring Security UserDetails from the PreAuthenticationToken created by our CustomHeaderAuthenticationFilter.

CustomAuthenticationUserDetailsService.

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.

CustomSecurityScope.

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();
}
}

CustomSecurityScopeProvider.

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 CustomAuthenticationUserDetailsService()
);
authenticationProvider = postProcess(authenticationProvider);

http
.authenticationProvider(authenticationProvider)
.setSharedObject(
AuthenticationEntryPoint.class,
new Http403ForbiddenEntryPoint()
);
}

@Override
public void configure(H http) {
AuthenticationManager authenticationManager
= http.getSharedObject(AuthenticationManager.class);

CustomHeaderAuthenticationFilter authenticationFilter
= new CustomHeaderAuthenticationFilter();
authenticationFilter.setAuthenticationManager(authenticationManager);
authenticationFilter = postProcess(authenticationFilter);

http.addFilter(authenticationFilter);
}
}
  • Register our CustomAuthenticationUserDetailsService with the out of the box PreAuthenticatedAuthenticationProvider

  • Register the authentication provider with Spring Security

  • Since all our requests are authenticated via our SSO we send back HTTP 403 if there was no authentication

  • 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
@EnableWebSecurity
public class SecurityConfiguration {

@Bean
@Order(10)
public SecurityFilterChain customDefaultSecurity(HttpSecurity http, ObjectProvider<FlowableHttpSecurityCustomizer> httpSecurityCustomizers) throws Exception {
for (FlowableHttpSecurityCustomizer customizer : httpSecurityCustomizers.orderedStream()
.collect(Collectors.toList())) {
customizer.customize(http);
}

http
.apply(new CustomHttpConfigurer<>())
.and()
.authorizeHttpRequests(configurer -> configurer
.requestMatchers(antMatcher("/analytics-api/**")).hasAuthority(SecurityConstants.ACCESS_REPORTS_METRICS)
.requestMatchers(antMatcher("/work-object-api/**")).hasAuthority(SecurityConstants.ACCESS_WORKOBJECT_API)
// allow context root for all (it triggers the loading of the initial page)
.requestMatchers(antMatcher("/")).permitAll()
.requestMatchers(
antMatcher("/**/*.svg"), antMatcher("/**/*.ico"), antMatcher("/**/*.png"), antMatcher("/**/*.woff2"), antMatcher("/**/*.css"),
antMatcher("/**/*.woff"), antMatcher("/**/*.html"), antMatcher("/**/*.js"),
antMatcher("/**/flowable-frontend-configuration"),
antMatcher("/**/index.html")).permitAll()
.anyRequest().authenticated()
);

return http.build();
}

@Bean
public CustomSecurityProvider() {
return new CustomSecurityProvider();
}
}
  • Apply our CustomConfigurer to the Spring Security configuration

  • 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 named AcmeUserDetails. It contains the tenantId for your user.

  • Your groups are mapped as a custom GroupGrantedAuthority and that you have other GrantedAuthority(ies) which represent something else for your organization.

For this we will have the following implementations

AcmeSecurityScope.

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();
}
}

AcmeSecurityScopeProvider.

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();
}
}

Custom Identity Service

This document describes how you can create your own Custom PlatformIdentityService and thus avoid syncing your users into the Flowable database. We assume that you are already familiar with the basics defined in Security and Identity Management and Overriding the Default Identity Service. If you need to create a custom authentication then have a look at Custom Security Authentication.

We are only going to implement the retrieve methods, nothing else. For this example, we are going to assume that your users are going to be retrieved from LDAP and we are going to use Spring LDAP to do the querying.

AcmeFlowablePlatformIdentityService.

public class AcmeFlowablePlatformIdentityService extends ReadOnlyIdmIdentityService {

protected LdapTemplate ldapTemplate;

public AcmeFlowablePlatformIdentityService(
CoreIdmEngineConfiguration idmEngineConfiguration,
LdapTemplate ldapTemplate
) {
super(idmEngineConfiguration);
this.ldapTemplate = ldapTemplate;
}

// Retrieve methods

@Override
public PlatformUserQuery createPlatformUserQuery() {
return new AcmePlatformUserQuery(commandExecutor, ldapTemplate);
}

@Override
public PlatformGroupQuery createPlatformGroupQuery() {
return new AcmePlatformGroupQuery(commandExecutor, ldapTemplate);
}

@Override
public List<String> findUniqueTenantIds() {
return Collections.emptyList();
}

}

For the purposes of this example we are going to show the implementation of only of subsets of methods of the AcmePlatformUserQuery and AcmePlatformGroupQuery.

AcmePlatformUserQuery.

public class AcmePlatformUserQuery
extends AbstractQuery<PlatformUserQuery, PlatformUser>
implements PlatformUserQuery {

protected LdapTemplate ldapTemplate;

protected String userId;
protected String displayName;
protected String displayNameLike;
protected boolean includeIdentityInfo;

public AcmePlatformUserQuery(CommandExecutor commandExecutor, LdapTemplate ldapTemplate) {
super(commandExecutor);
this.ldapTemplate = ldapTemplate;
}

@Override
public PlatformUserQuery includeIdentityInfo() {
this.includeIdentityInfo = true;
return this;
}

@Override
public PlatformUserQuery userId(String id) {
userId = id;
return this;
}

@Override
public PlatformUserQuery userDisplayName(String displayName) {
this.displayName = displayName;
return this;
}

@Override
public PlatformUserQuery userDisplayNameLike(String displayNameLike) {
this.displayNameLike = displayNameLike;
return this;
}

@Override
public PlatformUserQuery userDisplayNameLikeIgnoreCase(String displayNameLikeIgnoreCase) {
this.displayNameLike = displayNameLikeIgnoreCase;
return this;
}

// ...

@Override
public long executeCount(CommandContext commandContext) {
return executeList(commandContext).size();
}

@Override
public List<PlatformUser> executeList(CommandContext commandContext) {
LdapQueryBuilder queryBuilder = LdapQueryBuilder.query();

ContainerCriteria criteria = queryBuilder.where("objectClass").is("user");
if (userId != null) {
criteria = criteria.and("sAMAccountName").is(userId);
}

if (displayName != null) {
criteria = criteria.and("displayName").is(displayName);
}

if (displayNameLike != null) {
criteria = criteria.and("displayName").like(displayNameLike.replace('%', '*'));
}

return ldapTemplate.search(criteria, new AttributesMapper<PlatformUser>() {

@Override
public PlatformUser mapFromAttributes(Attributes attributes) throws NamingException {
BasicPlatformUser user = new BasicPlatformUser();
user.setIdentityInfo(new ArrayList<>());

user.setId(getAttribute("sAMAccountName", attributes));
user.setFirstName(getAttribute("givenName", attributes));
user.setLastName(getAttribute("sn", attributes));
user.setDisplayName(getAttribute("displayName", attributes));
user.setEmail(getAttribute("mail", attributes));
String dn = getAttribute("distinguishedName", attributes);
BasicPlatformIdentityInfo<String> dnInfo = BasicPlatformIdentityInfo.userInfo("dn", user.getId());

dnInfo.setTenantId(user.getTenantId());
dnInfo.setValue(value);
dnInfo.setTypeName(StringType.TYPE_NAME);

userEntity.getIdentityInfo().add(dnInfo));
if (includeIdentityInfo) {
CoreIdmEngineConfiguration idmEngineConfiguration = CommandContextUtil.getIdmEngineConfiguration(commandContext);
List<PlatformIdentityInfo> userDbInfos = idmEngineConfiguration.getIdmIdentityService().createPlatformIdentityInfoQuery()
.userId(user.getId())
.tenantId(user.getTenantId())
.list();
user.getIdentityInfo().addAll(userDbInfos);
}
return user;
}
});
}

protected static String getAttribute(String attributeName, Attributes attributes) {
Attribute attribute = attributes.get(attributeName);
if (attribute != null && attribute.size() > 0) {
try {
return (String) attribute.get();
} catch (NamingException e) {
throw new FlowableException("Exception getting attribute " + attributeName);
}
} else {
return null;
}
}
}

AcmePlatformGroupQuery.

public class AcmePlatformGroupQuery
extends AbstractQuery<PlatformGroupQuery, PlatformGroup>
implements PlatformGroupQuery {

protected LdapTemplate ldapTemplate;

protected String groupId;
protected Collection<String> groupIds;
protected String userId;

public AcmePlatformGroupQuery(CommandExecutor commandExecutor, LdapTemplate ldapTemplate) {
super(commandExecutor);
this.ldapTemplate = ldapTemplate;
}

@Override
public PlatformGroupQuery groupId(String groupId) {
this.groupId = groupId;
return this;
}

@Override
public PlatformGroupQuery groupIds(List<String> groupIds) {
this.groupIds = groupIds;
return this;
}

@Override
public PlatformGroupQuery groupKey(String groupKey) {
this.groupId = groupKey;
return this;
}

@Override
public PlatformGroupQuery groupMember(String groupMemberUserId) {
this.userId = groupMemberUserId;
return this;
}

// ...

@Override
public long executeCount(CommandContext commandContext) {
return executeList(commandContext).size();
}

@Override
public List<PlatformGroup> executeList(CommandContext commandContext) {
LdapQueryBuilder queryBuilder = LdapQueryBuilder.query();

ContainerCriteria criteria = queryBuilder.where("objectClass").is("group");
if (groupId != null) {
criteria = criteria.and("sAMAccountName").is(groupId);
}

if (userId != null) {
// LDAP requires the User Distinquished name, so we look it up
PlatformUser user = new AcmePlatformUserQuery(commandExecutor, ldapTemplate).userId(userId).singleResult();
String userDn = user.findString("distinguishedName");

// 1.2.840.113556.1.4.1941 is the OID for the LDAP_MATCHING_RULE_IN_CHAIN
// See more at https://ldapwiki.com/wiki/1.2.840.113556.1.4.1941
criteria = criteria.and("member:1.2.840.113556.1.4.1941:").is(userDn);
}

return ldapTemplate.search(criteria, new AttributesMapper<PlatformGroup>() {

@Override
public PlatformGroup mapFromAttributes(Attributes attributes) throws NamingException {
BasicPlatformGroup group = new BasicPlatformGroup();

group.setId(getAttribute("distinguishedName", attributes));
group.setKey(getAttribute("sAMAccountName", attributes));
group.setName(getAttribute("name", attributes));
return groupEntity;
}
});
}

protected static String getAttribute(String attributeName, Attributes attributes) {
Attribute attribute = attributes.get(attributeName);
if (attribute != null && attribute.size() > 0) {
try {
return (String) attribute.get();
} catch (NamingException e) {
throw new FlowableException("Exception getting attribute " + attributeName);
}
} else {
return null;
}
}
}

We now need to register our AcmePlatformIdentityService with the Core IDM Engine. This is done by using the EngineConfigurationConfigurer as follows:

Register AcmePlatformIdentityService with the Core IDM Engine.

@Configuration
public class AcmeFlowableConfiguration {

@Bean
public EngineConfigurationConfigurer<CoreIdmEngineConfiguration> acmeCoreIdmEngineConfigurer(LdapTemplate ldapTemplate) {
return idmEngineConfiguration -> {
idmEngineConfiguration.setIdmIdentityService(
new AcmePlatformIdentityService(idmEngineConfiguration, ldapTemplate)
);
};
}
}