Skip to main content

Example Spring Boot

In the first 4 steps, we will set up KADAI REST API without security. Then, we will show how to use the resulting REST API with Postman. This guide then sets up security. At the end of the guide, we will add the UI.

What you'll need

To set up the example, please install:

  • an IDE of your choice (IntelliJ recommended)
  • Java 17
  • maven
  • Postman or any similar tool for creating API requests

Note: Please name your packages, folders, and files exactly like in the example!

Set up KADAI REST-API without security

Step 1: Initialize an empty project

Go to Spring Initializer and create an example Maven Project. Choose the same options as in the Screenshot, except the spring version. Please check Java 17, then click on " Generate".

empty spring boot project

Unpack the project and open it in an IDE of your choice. Your project-structure should look like below:

demo
├───.mvn
│ └───wrapper
├───src
│ ├───main
│ └───test
│ .gitignore
│ HELP.md
│ mvnw
│ mvnw.cmd
│ pom.xml

Step 2: Add dependencies

Please add the following dependencies to the pom. All dependencies can be copied as one block at the end of step 2. After adding the dependencies, please reload maven and recompile the project.

** 1. spring core dependency: **

pom.xml
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<version>3.0.0</version>
</dependency>

** 2. database dependencies: **

pom.xml
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

** 3. kadai dependencies: **

pom.xml
<dependency>
<groupId>io.kadai</groupId>
<artifactId>kadai-common-data</artifactId>
<version>9.2.0</version>
</dependency>
<dependency>
<groupId>io.kadai</groupId>
<artifactId>kadai-common-logging</artifactId>
<version>9.2.0</version>
</dependency>
<dependency>
<groupId>io.kadai</groupId>
<artifactId>kadai-rest-spring</artifactId>
<version>9.2.0</version>
</dependency>

** 4. tomcat application server dependency: **

pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

** All dependencies **

pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.kadai</groupId>
<artifactId>kadai-common-logging</artifactId>
<version>9.2.0</version>
</dependency>
<dependency>
<groupId>io.kadai</groupId>
<artifactId>kadai-rest-spring</artifactId>
<version>9.2.0</version>
</dependency>
<dependency>
<groupId>io.kadai</groupId>
<artifactId>kadai-common-data</artifactId>
<version>9.2.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

Step 3: Add properties configuration

Step 3a: Fill out application.properties

The example already has the configuration file application.properties in the src/main/resources folder. It's a standard configuration file used by spring. You can read more about spring configuration in the spring documentation. You need to add the following content into that file:

src/main/resources/application.properties
logging.level.io.kadai=INFO
logging.level.org.springframework=INFO
server.servlet.context-path=/kadai
kadai.routing.dmn.upload.path=/tmp/routing.dmn
######## Kadai DB #######
######## h2 configuration ########
spring.datasource.url=jdbc:h2:mem:kadai;NON_KEYWORDS=KEY,VALUE;IGNORECASE=TRUE;DB_CLOSE_ON_EXIT=FALSE;LOCK_MODE=0
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=sa
kadai.schemaName=KADAI
######## h2 console configuration ########
########spring.h2.console.enabled=true
########spring.h2.console.path=/h2-console
######## db2 configuration ########
########spring.datasource.driverClassName=com.ibm.db2.jcc.DB2Driver
########spring.datasource.url=jdbc:db2://localhost:5101/tskdb
########spring.datasource.username=db2inst1
########spring.datasource.password=db2inst1-pwd
######## Postgres configuration ########
########spring.datasource.url=jdbc:postgresql://localhost:5102/postgres
########spring.datasource.driverClassName=org.postgresql.Driver
########spring.datasource.username=postgres
########spring.datasource.password=postgres
########spring.jpa.generate-ddl=true
########spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
####### property that control rest api security deploy use true for no security.
devMode=true
# This property enables the support of XSRF tokens. This will not work together with devMode.
enableCsrf=false
####### property that control if the database is cleaned and sample data is generated
generateSampleData=true
####### cache static resources properties
spring.web.resources.cache.cachecontrol.cache-private=true
####### for upload of big workbasket- or classification-files
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
####### serve compressed files for faster UI loading times
server.compression.enabled=true
server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css,image/svg+xml
server.compression.min-response-size=10240
spring.main.allow-bean-definition-overriding=true
server.tomcat.max-http-form-post-size=-1
server.tomcat.max-save-post-size=-1
server.tomcat.max-swallow-size=-1
####### tomcat is not detecting the x-forward headers from bluemix as a trustworthy proxy
server.tomcat.remoteip.internal-proxies=.*
server.forward-headers-strategy=native
####### Properties for AccessIdController to connect to LDAP
kadai.ldap.serverUrl=ldap://localhost:10389
kadai.ldap.bindDn=uid=admin
kadai.ldap.bindPassword=secret
kadai.ldap.baseDn=ou=Test,O=KADAI
kadai.ldap.userSearchBase=cn=users
kadai.ldap.userSearchFilterName=objectclass
kadai.ldap.userSearchFilterValue=person
kadai.ldap.userFirstnameAttribute=givenName
kadai.ldap.userLastnameAttribute=sn
kadai.ldap.userFullnameAttribute=cn
kadai.ldap.userPhoneAttribute=phoneNumber
kadai.ldap.userMobilePhoneAttribute=mobileNumber
kadai.ldap.userEmailAttribute=email
kadai.ldap.userOrglevel1Attribute=orgLevel1
kadai.ldap.userOrglevel2Attribute=orgLevel2
kadai.ldap.userOrglevel3Attribute=someDepartement
kadai.ldap.userOrglevel4Attribute=orgLevel4
kadai.ldap.userIdAttribute=uid
kadai.ldap.userMemberOfGroupAttribute=memberOf
kadai.ldap.userPermissionsAttribute=permission
kadai.ldap.groupSearchBase=
kadai.ldap.groupSearchFilterName=objectclass
kadai.ldap.groupSearchFilterValue=groupofuniquenames
kadai.ldap.groupNameAttribute=cn
kadai.ldap.minSearchForLength=3
kadai.ldap.maxNumberOfReturnedAccessIds=50
kadai.ldap.groupsOfUser=uniquemember
kadai.ldap.permissionSearchBase=
kadai.ldap.permissionSearchFilterName=objectclass
kadai.ldap.permissionSearchFilterValue=groupofuniquenames
kadai.ldap.permissionNameAttribute=permission
kadai.ldap.permissionsOfUser=uniquemember
kadai.ldap.useDnForGroups=true
# Embedded Spring LDAP server
spring.ldap.embedded.base-dn=OU=Test,O=KADAI
spring.ldap.embedded.credential.username=uid=admin
spring.ldap.embedded.credential.password=secret
spring.ldap.embedded.ldif=classpath:example-users.ldif
spring.ldap.embedded.port=10389
spring.ldap.embedded.validation.enabled=false

Step 3b: Add kadai.properties

Create kadai.properties in the "resources" folder. This file contains KADAI specific configuration, like custom holidays, etc. Please copy the following content into kadai.properties:

src/main/resources/kadai.properties
kadai.roles.user=cn=ksc-users,cn=groups,OU=Test,O=KADAI | teamlead-1 | teamlead-2 | user-1-1 | user-1-2 | user-2-1 | user-2-2 | user-b-1 | user-b-2
kadai.roles.admin=admin | uid=admin,cn=users,OU=Test,O=KADAI
kadai.roles.business_admin=businessadmin | cn=business-admins,cn=groups,OU=Test,O=KADAI
kadai.roles.monitor=monitor | cn=monitor-users,cn=groups,OU=Test,O=KADAI
kadai.roles.task_admin=taskadmin
kadai.domains=DOMAIN_A|DOMAIN_B|DOMAIN_C|DOMAIN_TEST
kadai.user.minimalPermissionsToAssignDomains=READ | OPEN
kadai.classification.types=TASK|DOCUMENT
kadai.classification.categories.task=EXTERNAL| manual| autoMAtic| Process
kadai.classification.categories.document=EXTERNAL
kadai.jobs.maxRetries=3
kadai.jobs.batchSize=50
kadai.jobs.runEvery=P1D
kadai.jobs.firstRunAt=2018-07-25T08:00:00Z
kadai.jobs.cleanup.task.minimumAge=P14D
kadai.jobs.cleanup.history.simple.batchSize=50
kadai.jobs.history.cleanup.firstRunAt=2018-07-25T08:00:00Z
kadai.jobs.cleanup.history.simple.minimumAge=P14D
kadai.jobs.history.cleanup.runEvery=P1D
kadai.jobs.refresh.user.runEvery=P1D
kadai.jobs.refresh.user.firstRunAt=2018-07-25T23:00:00Z
kadai.workingTime.holidays.german.enabled=true
kadai.workingTime.holidays.german.corpus-christi.enabled=true
kadai.history.logger.name=AUDIT
kadai.routing.dmn=/dmn-table.dmn
# enable or disable the jobscheduler at all
# set it to false and no jobs are running
kadai.jobs.scheduler.enabled=false
# wait time before the first job run in millis
kadai.jobs.scheduler.initialStartDelay=100
# sleeping time befor the next job runs
kadai.jobs.scheduler.period=12
# timeunit for the sleeping period
# Possible values: MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS
kadai.jobs.scheduler.periodTimeUnit=HOURS
kadai.jobs.cleanup.task.enable=true
kadai.jobs.priority.task.enable=true
kadai.jobs.cleanup.workbasket.enable=true
kadai.jobs.refresh.user.enable=true
kadai.jobs.cleanup.history.simple.enable=false

Step 4: Add rest configuration

First, Add @ComponentScan({"io.kadai","com.example"}) as annotation above the class definition of the ExampleApplication and a corresponding import to this class. This will allow the application to find the necessary components.

Then, create a java class with the name ExampleRestConfiguration in the com.example.demo package. This class defines the Beans and their dependencies. Your project structure should look like this:

demo
├───.mvn
├───src
│ ├───main
│ ├───java
│ │ └───com
│ │ └───example
│ │ └───demo
│ │ DemoApplication.java
│ │ ExampleRestConfiguration.java
│ └───resources
│ application.properties
│ kadai.properties
├───target
│ .gitignore
│ HELP.md
│ mvnw
│ mvnw.cmd
│ pom.xml

Copy the following content into ExampleRestConfiguration.java:

src/main/com/example/demo/ExampleRestConfiguration.java
package com.example.demo;

import java.sql.SQLException;
import javax.sql.DataSource;
import org.h2.tools.Server;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import io.kadai.KadaiConfiguration;
import io.kadai.common.api.KadaiEngine;
import io.kadai.common.internal.configuration.DbSchemaCreator;
import io.kadai.sampledata.SampleDataGenerator;

@Configuration
public class ExampleRestConfiguration {

@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

@Bean
@DependsOn("kadaiConfiguration") // generate sample data after schema was inserted
public SampleDataGenerator generateSampleData(
KadaiConfiguration kadaiConfiguration,
DataSource dataSource,
@Value("${generateSampleData:true}") boolean generateSampleData)
throws SQLException {
DbSchemaCreator dbSchemaCreator =
new DbSchemaCreator(dataSource, kadaiConfiguration.getSchemaName());
dbSchemaCreator.run();
SampleDataGenerator sampleDataGenerator =
new SampleDataGenerator(dataSource, kadaiConfiguration.getSchemaName());
if (generateSampleData) {
sampleDataGenerator.generateSampleData();
}
return sampleDataGenerator;
}

@Bean
@DependsOn("generateSampleData")
public KadaiEngine getKadaiEngine(KadaiConfiguration kadaiConfiguration)
throws SQLException {
return KadaiEngine.buildKadaiEngine(kadaiConfiguration);
}

// only required to let the adapter example connect to the same database
@Bean(initMethod = "start", destroyMethod = "stop")
public Server inMemoryH2DatabaseaServer() throws SQLException {
return Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9095");
}

@Bean
@ConditionalOnMissingBean(KadaiConfiguration.class)
public KadaiConfiguration kadaiConfiguration(
DataSource dataSource,
@Qualifier("kadaiPropertiesFileName") String propertiesFileName,
@Qualifier("kadaiPropertiesDelimiter") String delimiter) {
return new KadaiConfiguration.Builder(dataSource, true, "KADAI")
.initKadaiProperties(propertiesFileName, delimiter)
.build();
}
}


Step 5: Try out the REST-API

Recompile the project and then start the DemoApplication in your IDE. You can now make the following request:

GET http://localhost:8080/kadai/api/v1/classifications

You should get a list of different Classifications the responses body. See a snippet of the expected response-body below:

{
"classifications": [
{
"classificationId": "CLI:000000000000000000000000000000000001",
"key": "L10000",
"applicationEntryPoint": "",
"category": "EXTERNAL",
"domain": "",
"name": "OLD-Leistungsfall",
"parentId": "",
"parentKey": "",
"priority": 999,
"serviceLevel": "P1D",
"type": "TASK",
"custom1": "VNR,RVNR,KOLVNR",
"custom2": "",
"custom3": "",
"custom4": "",
"custom5": "",
"custom6": "",
"custom7": "",
"custom8": ""
},
...
]
}

You can also request Tasks using the following command:

GET http://localhost:8080/kadai/api/v1/tasks

At this point, full KADAI REST-API functionality is available without security or authorization. They will be added in the next steps.

Set up KADAI Security

Our example application uses ldap for its authorization. First, add a security package into the com.example.demo package (in src/main/java/com/example/demo). The package will consist of one configurer class: BootWebSecurityConfigurer (will be replaced), and one example configuration ExampleWebSecurityConfig. The classes will be created in the next steps.

Step 6: Add security dependencies and stop disabling security

Add the following dependencies to your pom and reload maven:

pom.xml
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Then, set the devMode property in application.properties to false. This enables authorization checks. You also need to remove the following lines from the ExampleRestConfiguration.java:

src/main/com/example/demo/ExampleRestConfiguration.java
@Bean
@ConditionalOnMissingBean(KadaiConfiguration.class)
public KadaiConfiguration kadaiConfiguration(
DataSource dataSource,
@Qualifier("kadaiPropertiesFileName") String propertiesFileName,
@Qualifier("kadaiPropertiesDelimiter") String delimiter) {
return new KadaiConfiguration.Builder(dataSource, true, "KADAI")
.initKadaiProperties(propertiesFileName, delimiter)
.build();
}

This way, you don't disable security manually. Deleted because KadaiEngineConfiguration doesn't exist anymore

Step 7: Add BootWebSecurityConfigurer.java

Create BootWebSecurityConfigurer.java in the security folder and copy the following content into it:

src/main/com/example/demo/security/BootWebSecurityConfigurer.java
package com.example.demo.security;

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer;
import org.springframework.security.config.ldap.LdapPasswordComparisonAuthenticationManagerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import io.kadai.common.rest.SpringSecurityToJaasFilter;

/** Default basic configuration for kadai web example. */
@Configuration
public class BootWebSecurityConfigurer {

private final String ldapServerUrl;
private final String ldapBaseDn;
private final String ldapUserDnPatterns;
private final String ldapGroupSearchBase;
private final String ldapGroupSearchFilter;

private final boolean devMode;
private final boolean enableCsrf;

public BootWebSecurityConfigurer(
@Value("${kadai.ldap.serverUrl:ldap://localhost:10389}") String ldapServerUrl,
@Value("${kadai.ldap.baseDn:OU=Test,O=KADAI}") String ldapBaseDn,
@Value("${kadai.ldap.userDnPatterns:uid={0},cn=users}") String ldapUserDnPatterns,
@Value("${kadai.ldap.groupSearchBase:cn=groups}") String ldapGroupSearchBase,
@Value("${kadai.ldap.groupSearchFilter:uniqueMember={0}}") String ldapGroupSearchFilter,
@Value("${enableCsrf:false}") boolean enableCsrf,
@Value("${devMode:false}") boolean devMode) {
this.enableCsrf = enableCsrf;
this.ldapServerUrl = ldapServerUrl;
this.ldapBaseDn = ldapBaseDn;
this.ldapGroupSearchBase = ldapGroupSearchBase;
this.ldapGroupSearchFilter = ldapGroupSearchFilter;
this.ldapUserDnPatterns = ldapUserDnPatterns;
this.devMode = devMode;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
authorizeHttpRequests ->
authorizeHttpRequests
.requestMatchers("/css/**", "/img/**")
.permitAll()
.requestMatchers(HttpMethod.GET, "/docs/**")
.permitAll())
.cors(Customizer.withDefaults())
.addFilter(jaasApiIntegrationFilter())
.addFilterAfter(new SpringSecurityToJaasFilter(), JaasApiIntegrationFilter.class);

if (enableCsrf) {
CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
csrfTokenRepository.setCookiePath("/");
http.csrf(
csrf ->
csrf.csrfTokenRepository(csrfTokenRepository)
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()))
.addFilterAfter(new CsrfCookieFilter(), SpringSecurityToJaasFilter.class);
} else {
http.csrf(AbstractHttpConfigurer::disable).httpBasic(Customizer.withDefaults());
}

if (devMode) {
http.headers(
headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.authorizeHttpRequests(
authorizeHttpRequests ->
authorizeHttpRequests
.requestMatchers("/h2-console/**")
.permitAll()
.anyRequest()
.fullyAuthenticated())
.logout(logout -> logout.logoutSuccessUrl("http://localhost:4200/#").permitAll());
} else {
addLoginPageConfiguration(http);
}
http.requestCache(RequestCacheConfigurer::disable);
return http.build();
}

protected void addLoginPageConfiguration(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
authorizeHttpRequests -> authorizeHttpRequests.anyRequest().fullyAuthenticated())
.formLogin(
formLogin ->
formLogin
.loginPage("/login")
.failureUrl("/login?error")
.defaultSuccessUrl("/index.html")
.permitAll())
.logout(
logout ->
logout
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout")
.deleteCookies("JSESSIONID")
.permitAll());
}

@Bean
public LdapAuthoritiesPopulator authoritiesPopulator(
DefaultSpringSecurityContextSource contextSource) {
Function<Map<String, List<String>>, GrantedAuthority> authorityMapper =
recordVar -> new SimpleGrantedAuthority(recordVar.get("spring.security.ldap.dn").get(0));

DefaultLdapAuthoritiesPopulator populator =
new DefaultLdapAuthoritiesPopulator(contextSource, ldapGroupSearchBase);
populator.setGroupSearchFilter(ldapGroupSearchFilter);
populator.setSearchSubtree(true);
populator.setRolePrefix("");
populator.setAuthorityMapper(authorityMapper);
return populator;
}

@Bean
@Primary
public DefaultSpringSecurityContextSource defaultSpringSecurityContextSource() {
return new DefaultSpringSecurityContextSource(ldapServerUrl + "/" + ldapBaseDn);
}

@Bean
public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
SimpleAuthorityMapper grantedAuthoritiesMapper = new SimpleAuthorityMapper();
grantedAuthoritiesMapper.setPrefix("");
return grantedAuthoritiesMapper;
}

protected JaasApiIntegrationFilter jaasApiIntegrationFilter() {
JaasApiIntegrationFilter filter = new JaasApiIntegrationFilter();
filter.setCreateEmptySubject(true);
return filter;
}

@Bean
AuthenticationManager ldapAuthenticationManager(
BaseLdapPathContextSource contextSource, LdapAuthoritiesPopulator authorities) {
@SuppressWarnings("deprecation")
LdapPasswordComparisonAuthenticationManagerFactory factory =
new LdapPasswordComparisonAuthenticationManagerFactory(
contextSource, NoOpPasswordEncoder.getInstance());
factory.setUserDnPatterns(ldapUserDnPatterns);
factory.setLdapAuthoritiesPopulator(authorities);
factory.setAuthoritiesMapper(grantedAuthoritiesMapper());
factory.setPasswordAttribute("userPassword");
return factory.createAuthenticationManager();
}
}

Create CsrfCookieFilter.java in the security folder and copy the following content into it:

src/main/com/example/demo/security/CsrfCookieFilter.java
package com.example.demo.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;

final class CsrfCookieFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(
HttpServletRequest request,
@SuppressWarnings("NullableProblems") HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.getToken();

filterChain.doFilter(request, response);
}
}

Lastly, create SpaCsrfTokenRequestHandler.java in the security folder and copy the following content into it:

src/main/com/example/demo/security/SpaCsrfTokenRequestHandler.java
package com.example.demo.security;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.function.Supplier;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
import org.springframework.util.StringUtils;

final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();

@Override
public void handle(
HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
this.delegate.handle(request, response, csrfToken);
}

@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*/
if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
return super.resolveCsrfTokenValue(request, csrfToken);
}
/*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
return this.delegate.resolveCsrfTokenValue(request, csrfToken);
}
}

Step 8: Add ExampleWebSecurityConfig.java

ExampleWebSecurityConfig specifies beans that are used for authorization by spring. Create ExampleWebSecurityConfig.java in the security package and copy following content there:

src/main/com/example/demo/security/ExampleWebSecurityConfig.java
package com.example.demo.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
public class ExampleWebSecurityConfig {

@Bean
public CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return source;
}
}

Step 9: Add users

In order for security to work, we need to define ldap users. Please download the example-users.ldif file here:


Please unzip the example-users file and put it into the resources folder.

Your project structure should now look like this:

demo
├───.mvn
├───src
│ ├───main
│ ├───java
│ │ └───com
│ │ └───example
│ │ └───demo
│ │ │───security
│ │ │ BootWebSecurityConfigurer.java
│ │ │ CsrfCookieFilter.java
│ │ │ ExampleWebSecurityConfig.java
│ │ │ SpaCsrfTokenRequestHandler.java
│ │ │───DemoApplication.java
│ │ │───ExampleRestConfiguration.java
│ └───resources
│ application.properties
│ example-users.ldif
│ kadai.properties
├───target
│ .gitignore
│ HELP.md
│ mvnw
│ mvnw.cmd
│ pom.xml

Step 10: Try out the REST-API

First, restart the ExampleApplication. Try to make a request like in the previous step, for example:

GET http://localhost:8080/kadai/api/v1/tasks

You should get the 401 Unauthorized response. Now, you can authorize yourself using basicAuth. In Postman, go to the "Authorization" tab. There, select basicAuth and type admin as user and admin as password. Then you can make the following request:

GET http://localhost:8080/kadai/api/v1/tasks

It should return a list of Tasks in the responses body. See a snippet of the expected response-body below:

{
"tasks": [
{
"taskId": "TKI:000000000000000000000000000000000000",
"externalId": "ETI:000000000000000000000000000000000000",
"created": "2025-01-15T19:27:56.991Z",
"claimed": "2025-01-16T19:27:56.991Z",
"completed": null,
"modified": "2025-01-16T19:27:56.991Z",
"planned": "2025-01-15T19:27:56.991Z",
"received": "2025-01-16T19:27:56.991Z",
"due": "2025-01-16T19:27:56.991Z",
"name": "Task99",
"creator": "creator_user_id",
"note": "Some custom Note",
"description": "Lorem ipsum was n Quatsch dolor sit amet.",
...
},
...
]
}

Set up KADAI UI

Step 11: Add web dependencies

Add the following dependencies to your pom and reload maven:

pom.xml
<dependency>
<groupId>io.kadai</groupId>
<artifactId>kadai-web</artifactId>
<version>9.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Step 12: Add controllers

Add controllers package into the com.example.demo package (in src/main/java/com/example/demo). This package will contain the controllers for different paths. Our application needs the following three controllers:

  • LoginController
  • ResourcesController
  • ViewController

These will be added in the steps 12a, 12b and 12c.

Step 12a: Add LoginController.java

The LoginController will handle the login into kadai. It will need the templates/login.html in the resources folder. You can download the templates folder here:

Please unzip the templates folder and put it into the resources folder. Then, copy following code into LoginController.java:

src/main/com/example/demo/controllers/LoginController.java
package com.example.demo.controllers;

import org.springframework.core.Ordered;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Controller
public class LoginController implements WebMvcConfigurer {

@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}

Step 12b: Add ResourcesController.java

The ResourcesController handles resources like images and additional customizations. You'll need the static folder for it. You can download the static folder here:


Please unzip the static folder and copy it into resources. Additionally, there is the com.example.demo.controllers folder for further customizations. Please download it here:


Unzip the com folder and put it into resources. Then, please copy the following code into ResourcesController.java:

src/main/com/example/demo/controllers/ResourcesController.java
package com.example.demo.controllers;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import io.kadai.common.internal.util.ResourceUtil;

import java.io.IOException;

@Controller
public class ResourcesController {

public static final String KADAI_CUSTOMIZATION_FILE_NAME = "kadai-customization.json";

@GetMapping(
value = "/environments/data-sources/kadai-customization.json",
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> kadaiCustomization() throws IOException {
return ResponseEntity.ok(readResourceAsString(KADAI_CUSTOMIZATION_FILE_NAME));
}

// the environment-information.json file will be served via "static" folder
// @GetMapping(
// value = "/environments/data-sources/environment-information.json",
// produces = MediaType.APPLICATION_JSON_VALUE)
// public ResponseEntity<String> environmentInformation() throws Exception {
// return ResponseEntity.ok(readResourceAsString("environment-information.json"));
// }

private String readResourceAsString(String resource) throws IOException {
String resourceAsString = ResourceUtil.readResourceAsString(getClass(), resource);
if (resourceAsString == null) {
return "{}";
}
return resourceAsString;
}
}

Step 12c: Add ViewController.java

The ViewController manages the root view of KADAI. Copy following code into ViewController.java:

src/main/com/example/demo/controllers/ViewController.java
package com.example.demo.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/** The view controller. */
@Controller
public class ViewController {

@GetMapping(path = {"", "kadai/**"})
public String index() {
return "forward:/index.html";
}
}

Step 13: Add WebMvcConfig.java

Create WebMvcConfig.java in the com.example.demo package. It handles resources and messages of the application. Copy following content into WebMvcConfig.java:

src/main/com/example/demo/WebMvcConfig.java
package com.example.demo;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import jakarta.annotation.PostConstruct;
import java.util.List;

/**
* The Web MVC Configuration.
*/
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/", "classpath:/resources/",
"classpath:/static/", "classpath:/public/", "classpath:/templates/"
};

private final ObjectMapper objectMapper;

@Autowired
public WebMvcConfig(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!registry.hasMappingForPattern("/webjars/**")) {
registry
.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
if (!registry.hasMappingForPattern("/**")) {
registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
}
}

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter jacksonConverter =
(MappingJackson2HttpMessageConverter) converter;
jacksonConverter.setPrettyPrint(true);
}
}
}

@PostConstruct
public void enableObjectIndent() {
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
}
}

Your project structure should look like this:

demo
├───.mvn

├───src
│ ├───main
│ │ ├───java
│ │ │ └───com
│ │ │ └───example
│ │ │ └───demo
│ │ │ │
│ │ │ ├───controllers
│ │ │ │ LoginController.java
│ │ │ │ ResourcesController.java
│ │ │ │ ViewController.java
│ │ │ │
│ │ │ │───security
│ │ │ │ BootWebSecurityConfigurer.java
│ │ │ │ CsrfCookieFilter.java
│ │ │ │ ExampleWebSecurityConfig.java
│ │ │ │ SpaCsrfTokenRequestHandler.java
│ │ │ │───DemoApplication.java
│ │ │ │───ExampleRestConfiguration.java
│ │ │ │───WebMvcConfig.java
│ │ └───resources
│ │ │ application.properties
│ │ │ example-users.ldif
│ │ │ kadai.properties
│ │ │
│ │ ├───com
│ │ │ └───example
│ │ │ └───demo
│ │ │ └───controllers
│ │ │ kadai-customization.json
│ │ │
│ │ ├───static
│ │ │ ├───css
│ │ │ ├───environments
│ │ │ └───img
│ │ └───templates
│ └───test
│ .gitattributes
│ .gitignore
│ HELP.md
│ mvnw
│ mvnw.cmd
│ pom.xml

Step 14: Start and open the application

Recompile the application, then go to the DemoApplication class in the IDE and start it. Then type localhost:8080/kadai into your browser. You should see the login screen:

Log in

Log in using admin as username and admin as password. Now, you should see the following:

Workbaskets

Press the menu button in the upper left to navigate.

Navigate