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

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 would be to write a custom AuthenticationUserDetailsService that would load the 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.

Example 1. 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.

Example 2. Application Security Configuration
@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 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

Example 3. Acme Flowable Configuration
@Configuration
public class AcmeFlowableConfiguration {

    @Bean
    public AcmeSecurityProvider() {
    	return new AcmeSecurityProvider();
    }
}