Backend Application

Bootstrap App

In this section, you will use a web service called Spring Initializr to prepare the Spring Boot project.

To bootstrap your application

  1. Go to https://start.spring.io/ to use Spring Initializr.
  2. Enter the Project Metadata, as follows:
    • Group: the group ID, such as com.finastra
    • Artifact: the artifact ID, which is your application name, such as: events
  3. Click Generate. A ZIP archive, with the name of your artifact, is ready for you to download.
  4. Unpack the downloaded archive, and open the project in your favorite IDE.
  5. Open pom.xml. The <parent> version must be the same as the version of spring-boot-... dependencies. To do this automatically, add the spring-boot.version property as a variable in the <properties> field from the pom.xml file.
...
	<properties>
    ...
    	<spring-boot.version>SPRING_BOOT_VERSION</spring-boot.version>
	</properties>
...
  1. In pom.xml add a set of <dependencies> from Maven repository, apart from the Spring Initializr ones. Use the ${spring-boot.version} value for the spring-boot dependencies <version> key.
...
<dependencies>
	
<!-- dependencies added by Spring Initializr -->

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

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		
<!-- dependencies added from Maven repository -->

<!-- spring-boot dependencies -->

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>${spring-boot.version}</version>
    <scope>test</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>${spring-boot.version}</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot</artifactId>
    <version>${spring-boot.version}</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-websocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>${spring-boot.version}</version>
</dependency>


<!-- other mandatory dependencies -->

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-contract-wiremock -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-wiremock</artifactId>
    <version>2.2.2.RELEASE</version>
    <scope>test</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>8.17</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>

<!-- https://mvnrepository.com/artifact/io.swagger/swagger-core -->
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-core</artifactId>
    <version>1.6.1</version>
</dependency>


</dependencies>

...
  1. Update the <plugins> element as follows:
...

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
				<!-- https://mvnrepository.com/artifact/io.swagger/swagger-codegen-maven-plugin -->
      <plugin>
        <groupId>io.swagger</groupId>
        <artifactId>swagger-codegen-maven-plugin</artifactId>
        <version>2.4.14</version>
     		<executions>
					<execution>
						<goals>
							<goal>generate</goal>
						</goals>
						<configuration>
							<inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
							<language>spring</language>
							<configOptions>
								<sourceFolder>src/gen/java/main</sourceFolder>
								<java8>true</java8>
								<interfaceOnly>true</interfaceOnly>
							</configOptions>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

...
  1. Create a configuration file, named openapi.yaml, in /src/main/resources/. Enter the following content:
swagger: '2.0'
info:
  description: "."
  contact:
    name: API Support
    url: https://community.fusionfabric.cloud/index.html
  x-finastra-category: Other
  x-finastra-subcategory: ''
  x-finastra-tags: []
  x-finastra-maturity-level: BETA
  x-finastra-audience: PUBLIC
  x-finastra-commitId: 57e5d69943bdd6c4d49f3d422e8fe1b593761c17
  title: Clock Service Events
  version: 1.0.1
  x-finastra-short-description: Clock Service Events
  x-finastra-channel-type: B2B
paths:
  /sample/clock-service/v1/datetime-published:
    post:
      tags:
        - Clock event
      operationId: clock
      summary: This event occurs when a new UTC datetime is published.
      description: This event occurs every 15 seconds when a new UTC datetime in ISO-8601 format is published.
      consumes:
        - application/json
      parameters:
        - name: event
          in: body
          required: true
          schema:
            type: object
            required:
              - tenant
            properties:
              tenant:
                description: Identifier of the tenant
                type: string
              eventTime:
                description: UTC date and time of event expressed in ISO-8601 format
                type: string
            additionalProperties: true
            example:
              tenant: sandbox
              eventTime: '2019-10-21T13:07:28.219'
      responses:
        '204':
          description: No content
  "/sample/clock-service/v1":
    options:
      summary: Responds with header WebHook-Allowed-Origin to allow registering / as an event endpoint.
      operationId: validateEventEndpoint
      responses:
        200:
          description: OK
          headers:
            WebHook-Allowed-Origin:
              type: string
              description: Value must be "api.fusionfabric.cloud"
definitions: {}

This file is the YAML representation of the Events specification in OpenAPI format.You can find it under Actions menu. Make sure to convert it to YAML format for your project.

API Swagger files

You are now ready to code your event notifications application.

By setting up webhooks in your backend application you will receive notifications from FusionFabric.cloud any time the event occurs.

You will be using a code generator Maven plugin - swagger-codegen-maven-plugin from io.swagger, to help you implement the two endpoints of the Clock Service Events specification, namely:

  • POST /sample/clock-service/v1/datetime-published - to return the date and time at every 15 seconds.
  • OPTIONS /sample/clock-service/v1 - to allow FusionFabric.cloud to test your webhook URLs. This endpoint will return an HTTP header - WebHook-Allowed-Origin with the value api.fusionfabric.cloud.

You create 4 packages in the src/main/java/com.finastra.events/ directory:

  • controller package - which stores a REST controller object
  • configuration package - which stores configuration objects
  • exceptions package - which stores exception objects
  • filter package - which stores filtering objects applied to any incoming HTTP requests

Controller

To implement the controller package

  1. Create a package named controller.
  2. In Controller package, create the EventsApplicationController class with the following code:
package com.finastra.events.controller;


/**
* This class defines the endpoints on which the application listens
*/

@RestController
@RequestMapping
public class EventsApplicationController implements SampleApi {

	private static final String WEB_HOOK_ALLOWED_ORIGIN = "WebHook-Allowed-Origin";
	private static final String WEB_HOOK_FUSION_FABRIC_ORIGIN = "api.fusionfabric.cloud";
	private final SimpMessagingTemplate template;

	@Autowired
	EventsApplicationController(SimpMessagingTemplate template) {
		this.template = template;
	}

	@Override
	public ResponseEntity<Void> clock(Event event) {
		log.info("Event received {}", event.toString());
		// Send event to the endpoint
		this.template.convertAndSend("/clock/event",  event);
		// Return status code from the destination query
		return ResponseEntity.ok().build();
	}

	@Override
	public ResponseEntity<Void> validateEventEndpoint() {
		return ResponseEntity.ok()
				.header(WEB_HOOK_ALLOWED_ORIGIN, WEB_HOOK_FUSION_FABRIC_ORIGIN)
				.build();
	}
}
  1. Import the required libraries for EventsApplicationController above the class declaration.
import io.swagger.api.SampleApi;
import io.swagger.model.Event;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Configuration

To implement the configuration package

  1. Create a package named configuration.
  2. In the configuration package, create 2 classes:
  • MessagingConfiguration
  • WebSocketConfiguration
  1. Write the following code for the MessagingConfiguration class:
package com.finastra.events.configuration;

/**
* This class configures the messaging URL to retrieve its public key
*/

@Component
@ConfigurationProperties(prefix = "messaging")
public class MessagingConfiguration {
    private String serverUrl;

    public MessagingConfiguration() {
    }

    public String getServerUrl() {
        return serverUrl;
    }

    public void setServerUrl(String serverUrl) {
        this.serverUrl = serverUrl;
    }

}
  1. Import the required libraries for MessagingConfiguration above the class declaration.
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
  1. Write the following code for the WebSocketConfiguration class:
package com.finastra.events.configuration;

/**
* This class configures the web socket. It means that everytime the backend 
* receives the events, they are forwarded to the UI
*/


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {

    public static final String SOCKET_ENDPOINT = "/socket";

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(SOCKET_ENDPOINT)
                .setAllowedOrigins("*")
                .withSockJS();
    }
}
  1. Import the required libraries for the WebSocketConfiguration above the class declaration.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

Exceptions

To implement the exceptions package

  1. Create a package named exceptions.
  2. In exceptions package, create the JwksException class with the following code:
package com.finastra.events.exceptions;

/**
* This class defines the exception that is thrown when the signature of the event
* could not be validated
*/

public class JwksException extends RuntimeException {

    private static final String INVALID_JWKS_EXCEPTION = "JWKS exception: ";
    private static final long serialVersionUID = -7708447901493903974L;

    public JwksException(Exception e) {
        super(INVALID_JWKS_EXCEPTION, e);
    }

    public JwksException(String message) {
        super(INVALID_JWKS_EXCEPTION + message);
    }
}

Resource Sharing Filter

To implement the filter package

  1. Create a package named filter.
  2. In the filter package, create 4 classes:
  • CorsFilter
  • MutipleReadHttpRequest
  • SignatureCheckFilter
  • TraceFilter
  1. Write the following code for the CorsFilter class:
package com.finastra.events.filter;

/**
* This class defines the Cross Origin resource sharing filter to allow frontend to
* use backend
*/

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter extends GenericFilterBean {

    private static final String CLOCK_VALIDATION_URL = "/sample/clock-service/v1";
    @Value("${events-application.allowed-origin}")
    private List<String> allowedOrigins;

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;
        // Access-Control-Allow-Origin
        String origin = request.getHeader("Origin");
        response.setHeader("Access-Control-Allow-Origin", allowedOrigins.contains(origin) ? origin : "");
        response.setHeader("Vary", "Origin");

        // Access-Control-Max-Age
        response.setHeader("Access-Control-Max-Age", "3600");

        // Access-Control-Allow-Credentials
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
        boolean isWebHookValidation = request.getRequestURL().toString().endsWith(CLOCK_VALIDATION_URL);
        if (HttpMethod.OPTIONS.toString().equalsIgnoreCase((request).getMethod())
                && !isWebHookValidation) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }
}
  1. Import the required libraries for CorsFilter above the class declaration.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.List;
  1. Write the following code for the MultipleReadHttpRequest class:
package com.finastra.events.filter;

/**
* This class enables HttpRequests to be read twice for checking the signature for
* de-serialization, which is not allowed by default
*/

public class MultipleReadHttpRequest extends HttpServletRequestWrapper {
    private final byte[] body;

    MultipleReadHttpRequest(HttpServletRequest request, byte[] body) {
        super(request);
        this.body = body;
    }

    static byte[] readBody(ServletRequest request) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();

        try (BufferedReader bufferedReader = request.getReader()) {
            char[] charBuffer = new char[128];
            int bytesRead;
            while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                stringBuilder.append(charBuffer, 0, bytesRead);
            }
            return stringBuilder.toString().getBytes();
        }
    }

    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return true;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                throw new UnsupportedOperationException();
            }

            public int read() {
                return byteArrayInputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

}
  1. Import the required libraries for MultipleReadHttpRequest above the class declaration.
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import java.io.*;
  1. Write the following code for the SignatureCheckFilter class:
package com.finastra.events.filter;

/**
* This class defines a filter to check the signature header of the event to protect
* the application against events not sent by FFDC
*/

@Component
public class SignatureCheckFilter extends GenericFilterBean {

    private final static Logger LOG = LoggerFactory.getLogger(SignatureCheckFilter.class);

    private final RemoteJWKSet<SecurityContext> remoteJWKSet;

    @Autowired
    public SignatureCheckFilter(MessagingConfiguration messagingConfiguration) {
        try {
            final String jwksUrl = messagingConfiguration.getServerUrl() + "/jwks.json";
            LOG.info("server URL {}", messagingConfiguration.getServerUrl());
            this.remoteJWKSet = new RemoteJWKSet<>(new URL(jwksUrl));
        } catch (IOException e) {
            LOG.error("Failed to load jwks: ", e);
            throw new JwksException(e);
        }
    }

    /**
     * This method filter rejects incoming event not coming from FFDC services
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String url = httpRequest.getRequestURL().toString();
        boolean isWebSocket = (httpRequest.getMethod().equals("GET") || httpRequest.getMethod().equals("POST"))
                && url.contains("/socket/");
        boolean isWebHookValidation = url.endsWith("/sample/clock-service/v1");
        if (isWebSocket || isWebHookValidation) {
            chain.doFilter(request, response);
            return;
        }
        byte[] body = MultipleReadHttpRequest.readBody(httpRequest);
        MultipleReadHttpRequest wrappedRequest = new MultipleReadHttpRequest(httpRequest, body);
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String signature = wrappedRequest.getHeader("Signature");
        try {
            // check signature
            checkSignature(signature, body);
            chain.doFilter(wrappedRequest, response);
        } catch (JwksException e) {
            LOG.error("signature check failed", e);
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
        }
    }

    /**
     * This method checks Signature of dataByte that matches the FFDC public Key
     *
     * @param fullSignature the full signature containing the key id, the algorithm and the signature
     * @param dataByte      the signed data
     */
    private void checkSignature(String fullSignature, byte[] dataByte) {
        try {
            Signature sig = parseSignature(fullSignature);
            final JWKMatcher jwkMatcher = new JWKMatcher.Builder()
                    .keyID(sig.keyId)
                    .algorithm(sig.algorithm)
                    .build();
            final List<JWK> jwkList = remoteJWKSet.get(new JWKSelector(jwkMatcher), null); // security context is ignored for Remote JWK set
            if (jwkList.size() != 1) {
                LOG.info("jwkList{}", jwkList.toString());
                throw new JwksException("Unable to find unique matcher for keyId:"
                        + sig.keyId + ", and algorithm:" + sig.algorithm + " in provided JWKS.");
            }
            final JWK jwk = jwkList.get(0);
            final JWSVerifier verifier = new RSASSAVerifier((RSAKey) jwk);
            LOG.info("Signature {}", sig.signature);
            final Base64URL signatureBaseURL =
                    Base64URL.encode(Base64.getMimeDecoder().decode(sig.signature));
            final JWSHeader jwsHeader = new JWSHeader(new JWSAlgorithm(jwk.getAlgorithm().getName()));
            if (!verifier.verify(jwsHeader, dataByte, signatureBaseURL)) {
                throw new JwksException("Invalid signature.");
            }
        } catch (JOSEException e) {
            throw new JwksException(e);
        }
    }

    private Signature parseSignature(String fullSignature) {
        String[] signaturesVal = fullSignature.split(",");
        if (signaturesVal.length == 3) {
            String[] keyVal = signaturesVal[0].split("=");
            String[] algorithmVal = signaturesVal[1].split("=");
            String[] signVal = signaturesVal[2].split("=");
            final String keyId = keyVal[1].replace("\"", "");
            final String algorithm = algorithmVal[1].replace("\"", "");
            final String signature = signVal[1].replace("\"", "");
            return new Signature(keyId, algorithm, signature);
        }
        throw new JwksException("Unable to parse header " + fullSignature);
    }

    private static class Signature {
        private final String keyId;
        private final Algorithm algorithm;
        private final String signature;

        private Signature(String keyId, String algorithm, String signature) {
            this.keyId = keyId;
            this.algorithm = new Algorithm(algorithm);
            this.signature = signature;
        }
    }

}
  1. Import the required libraries for SignatureCheckFilter above the class declaration.
import com.finastra.events.configuration.MessagingConfiguration;
import com.finastra.events.exceptions.JwksException;

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKMatcher;
import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.util.Base64URL;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.net.URL;
import java.util.Base64;
import java.util.List;
  1. Write the following code for the TraceFilter class:
package com.finastra.events.filter;

/**
* This class adds diagnostic context to the logs
*/

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceFilter extends GenericFilterBean {

    public static final String FF_TRACE_ID = "ff-trace-id";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String ffTraceId = httpRequest.getHeader(FF_TRACE_ID);
        if (ffTraceId != null) {
            try (MDC.MDCCloseable mdcCloseable = MDC.putCloseable(FF_TRACE_ID, ffTraceId)) {
                chain.doFilter(httpRequest, response);
            }
        } else {
            chain.doFilter(httpRequest, response);
        }
    }
}
  1. Import the required libraries for TraceFilter above the class declaration.
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

import java.io.IOException;

Application Properties

To configure the application properties

  1. In /src/main/resources open the application.properties file, created by default
  2. Add the following content:
messaging.serverUrl=https://api.fusionfabric.cloud/messaging/v1
events-application.allowed-origin=http://localhost:4200

logging.level.org.springframework=ERROR
logging.level.com.mkyong=DEBUG
logging.pattern.console=%d{HH:mm:ss.SSS} [%t] %-5level %-36mdc{ff-trace-id:-} %logger{36} - %msg%n

This is how your backend project should look like at the end:

Events App Backend Project