Flowable provides a set of Spring Boot starters to help you embed the different engines (i.e., BPMN, DMN and CMMN) and to expose their RESTful APIs.

In this post, I'll walk you through the steps I followed to create a RESTful API (Resource Server) that embeds Flowable's BPMN engine, exposes the BPMN engine's RESTful API and leverages Spring Security’s support for OAuth 2.0 and Jason Web Tokens (JWTs).

Spring Boot

Getting Started

According to the Spring Boot Getting Started guide you should use the Spring Initializr to bootstrap your application:

I added the following dependencies: Spring Web, Spring Data JPA, H2 Database, Spring HAETOAS, Spring Security and OAuth2 Resource Server.

Flowable

In order to embed Flowable's BPMN engine and expose the BPMN engine's RESTful API we need to update the project's pom.xml:

  <dependencies>
  
    ...
    
    <dependency>
      <groupId>org.flowable</groupId>
      <artifactId>flowable-spring-boot-starter</artifactId>
      <version>${flowable.version}</version>
    </dependency>

    <dependency>
      <groupId>org.flowable</groupId>
      <artifactId>flowable-spring-boot-starter-process-rest</artifactId>
      <version>${flowable.version}</version>
    </dependency>
   
  </dependencies>

By default, certain folders on the classpath are automatically scanned:

  • /apps: Looks for all files ending with .zip or .bar and deploys them
  • /cases: Looks for all files ending with .cmmn, .cmmn11, .cmmn.xml or .cmmn11.xml and deploys them
  • /dmn: Looks for all files ending with .dmn, .dmn11, .dmn.xml or .dmn12.xml and deploys them
  • /forms: Looks for all files ending with .form and deploys them
  • /processes: Looks for all files ending with .bpmn20.xml or .bpmn and deploys them

For example:

├── /src
    └── /main
        └── /java
        └── /resources
            └── /apps
                ├── hr-app.zip
            └── /processes
            ├── application.properties
    └── /test

Spring Security

I extended Spring Security's WebSecurityConfigurerAdapter:

package org.serendipity.restapi.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
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.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

@EnableWebSecurity
public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwkSetUri;

  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    
    // H2 console configuration
    
    httpSecurity.authorizeRequests().antMatchers("/h2-console/**").permitAll();
    httpSecurity.csrf().disable();
    httpSecurity.headers().frameOptions().disable();
    
    // OAuth2 Resource Server configuration

    httpSecurity.authorizeRequests().anyRequest().authenticated();
    httpSecurity.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
  }

  @Bean
  JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build();
  }

}

And updated the application.properties:

spring.main.banner-mode=off
spring.jpa.open-in-view=false

server.port=3001

# Spring Security
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:10001/auth/realms/development/protocol/openid-connect/certs

# Logging
logging.level.root=INFO
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
logging.level.org.springframework.security=DEBUG

# Spring JPA
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:~/serendipity-db/db;AUTO_SERVER=TRUE;AUTO_SERVER_PORT=9091;DB_CLOSE_DELAY=-1
spring.datasource.username=admin
spring.datasource.password=secret
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop

# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false

To point to my Authorization Server's JWK Set Uri.

For example:

curl http://localhost:10001/auth/realms/development/protocol/openid-connect/certs

Sample output:

{
    "keys": [
        {
            "kid": "5E-jGIlDdpgjnhJzbdS3XXeZWZCmRUB85Snfp5IwyjI",
            "kty": "RSA",
            "alg": "RS256",
            "use": "sig",
            "n": "t3609odH5dIcPZUd4yFTjilR89MigdbgwnzFII82hzeazIsZivoSVWTfWG4620-zYN5PyFPKsaW_0IF7MPCeDXgRZ0odiizRQdhczZQA-zVrlqxy93SqRpjqRd_F3bOmwQZAsdepQefGNrTIArpq62s5ycUQTf-qbzsnCQugb5_SRa7u1VJaBgb0jTM-L304TSiW1vUFg2Th4fyQqVL4xrJuBPrrJCBs9nx9GPJDD8fXZtDMLlXRYhzZjW8zfqHSyA46JCDbK4-Tt6Ra6dNNKz8n2leSbUcf9NgBsiGHn23SnkM5GzbKbI-i_oQTvueP2psZzm8_Oyi3KYLtjNwBWw",
            "e": "AQAB",
            "x5c": [
                "MIICrTCCAZUCBgFvIWzNBjANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9TZXJlbmRpcGl0eSBDRVAwHhcNMTkxMjIwMDM0NzU2WhcNMjkxMjIwMDM0OTM2WjAaMRgwFgYDVQQDDA9TZXJlbmRpcGl0eSBDRVAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3frT2h0fl0hw9lR3jIVOOKVHz0yKB1uDCfMUgjzaHN5rMixmK+hJVZN9YbjrbT7Ng3k/IU8qxpb/QgXsw8J4NeBFnSh2KLNFB2FzNlAD7NWuWrHL3dKpGmOpF38Xds6bBBkCx16lB58Y2tMgCumrraznJxRBN/6pvOycJC6Bvn9JFru7VUloGBvSNMz4vfThNKJbW9QWDZOHh/JCpUvjGsm4E+uskIGz2fH0Y8kMPx9dm0MwuVdFiHNmNbzN+odLIDjokINsrj5O3pFrp000rPyfaV5JtRx/02AGyIYefbdKeQzkbNspsj6L+hBO+54/amxnObz87KLcpgu2M3AFbAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFVnIzwaPDVTvQ3q1QRczCBG348SqpDV1rrh5omMTwiqDI6uVWqgz7ij4I6XkS2w3B+eccxecE5cio0ZkRoxT1Ft4gZt5rzOIVch0nGIJp7NgTd5OKXE3BfJ0hcji+QPMosB7VKBWC82zu/n13DLoNzVn0qPUvh/1cvg/3cjDE5WKqPzaJleoRSdQhATMo5PyyqRkNVaqObuFWJffrErJNRFfeFXUFQvstSxcOmdKkuGZcMlLKbfug5qfAHK+0xCL/nOCMZizEhnAZqA7qzXqaM7NMUg48XN6x0z1Vxz5IDDfpVlN9TbDnNiSGiPJLENY5rm31kDjm9iHp4FrHF8FzI="
            ],
            "x5t": "bHB0yas9UeCu3AzfcE0Uu8omPhY",
            "x5t#S256": "otHl46Z-oZbRfHnTezn5gtr1rjHUhmWTvGYV3g_q8lc"
        }
    ]
}

Now we can build the application:

mvn clean
mvn package

And launch it:

java -jar target/serendipity-rest-api-core-0.0.1-SNAPSHOT.jar

We can use Postman to obtain an access token from Keycloak:

We can also use Postman to test the application's RESTful API:

package org.serendipity.restapi.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;

import org.serendipity.restapi.model.Individual;
import org.serendipity.restapi.service.IndividualService;

import org.serendipity.restapi.hateoas.IndividualRepresentationModelAssembler;

@RestController
@RequestMapping("/api")
public class IndividualController {
  
  @Autowired
  private IndividualService entityService;
  
  @Autowired
  private IndividualRepresentationModelAssembler assembler;
  
  @GetMapping("/whoami")
  public String whoami(@AuthenticationPrincipal Jwt jwt) {
    return String.format("Hello, %s!", jwt.getSubject());
  }
  
  @GetMapping("/individuals")
  @PreAuthorize("hasAuthority('SCOPE_individual:read')")
  public ResponseEntity<CollectionModel<EntityModel<Individual>>> findAll() {
    
    return ResponseEntity.ok(assembler.toCollectionModel(entityService.findAll()));
  }
  
  @GetMapping("/individuals/{id}")
  @PreAuthorize("hasAuthority('SCOPE_individual:read')")
  public ResponseEntity<EntityModel<Individual>> findById(
      @PathVariable("id") final Long id) throws ResponseStatusException {
    
    Individual entity = entityService.findById(id).orElseThrow(() -> 
        new ResponseStatusException(HttpStatus.NOT_FOUND));
    
    return ResponseEntity.ok(assembler.toModel(entity));
  }
  
}

For example:

And to test the BPMN engine's RESTful API:

We can also navigate to the H2 console:

Flowable UI Applications

  • Flowable Identity Management
  • Flowable Modeler
  • Flowable Task
  • Flowable Admin

You can download the Flowable open source distribution from the Flowable web site.

Externalised Configuration

The Flowable Web applications take advantage of Spring Boot's support for externalised configuration:

spring.main.banner-mode=off

# Logging
logging.level.root=INFO
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
logging.level.org.springframework.security=DEBUG

# Spring JPA
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:~/serendipity-db/db;AUTO_SERVER=TRUE;AUTO_SERVER_PORT=9091;DB_CLOSE_DELAY=-1
spring.datasource.username=admin
spring.datasource.password=secret
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update

# Default Admin Accounts
flowable.idm.app.admin.user-id=flowable
flowable.idm.app.admin.password=secret
flowable.idm.app.admin.first-name=
flowable.idm.app.admin.last-name=Administrator
flowable.idm.app.admin.email=admin@serendipity.org.au

flowable.common.app.idm-admin.user=flowable
flowable.common.app.idm-admin.password=secret

flowable.modeler.app.deployment-api-url=http://localhost:9999/flowable-task/app-api

# LDAP
flowable.idm.ldap.enabled=true
flowable.idm.ldap.server=ldap://localhost
flowable.idm.ldap.port=10389
flowable.idm.ldap.user=cn=admin,dc=flowable,dc=org
flowable.idm.ldap.password=secret
flowable.idm.ldap.base-dn=dc=flowable,dc=org
flowable.idm.ldap.user-base-dn=ou=users,dc=flowable,dc=org
flowable.idm.ldap.group-base-dn=ou=groups,dc=flowable,dc=org
flowable.idm.ldap.query.user-by-id=(&(objectClass=inetOrgPerson)(uid={0}))
flowable.idm.ldap.query.user-by-full-name-like=(&(objectClass=inetOrgPerson)(|({0}=*{1}*)({2}=*{3}*)))
flowable.idm.ldap.query.all-users=(objectClass=inetOrgPerson)
flowable.idm.ldap.query.groups-for-user=(&(objectClass=groupOfUniqueNames)(uniqueMember={0}))
flowable.idm.ldap.query.all-groups=(objectClass=groupOfUniqueNames)
flowable.idm.ldap.query.group-by-id=(&(objectClass=groupOfUniqueNames)(uniqueId={0}))
flowable.idm.ldap.attribute.user-id=uid
flowable.idm.ldap.attribute.first-name=cn
flowable.idm.ldap.attribute.last-name=sn
flowable.idm.ldap.attribute.email=mail
flowable.idm.ldap.attribute.group-id=cn
flowable.idm.ldap.attribute.group-name=cn
flowable.idm.ldap.cache.group-size=10000
flowable.idm.ldap.cache.group-expiration=180000

Flowable Identity Management

To launch Flowable's Identity Management application:

java -jar flowable-idm.war

Then navigate to: http://localhost:8080/flowable-idm

Flowable Modeler

To launch Flowable's Modeler application:

java -jar flowable-modeler.war

Then navigate to: http://localhost:8888/flowable-modeler

Flowable Task

To launch Flowable's Task application:

java -jar flowable-task.war

Then navigate to: http://localhost:9999/flowable-task

Flowable Admin

To launch Flowable's Admin application:

java -jar flowable-admin.war

Then navigate to: http://localhost:9988/flowable-admin

Source Code:
References: