Chemaxon Synergy integration workshop

    Table of Contents

    1. Prerequisites

    {info} Optionally check these to verify settings:

    
    java -version
    java version "1.8.x"
    Java(TM) SE Runtime Environment (build 1.8.x)
    Java HotSpot(TM) 64-Bit Server VM ...
    
    spring --version
    Spring CLI v1.5.3.RELEASE
    
    git --version
    git version 2.9.0.
    
    heroku --version
    heroku-cli/5.9.1-3d5ebd1 ... go1.7.5

    2. Generate a Spring Boot app

    2.1 Create a Spring Boot application

    
    spring init --package-name=com.example --dependencies=web,actuator --boot-version=1.5.6.RELEASE example-synergy-app
    cd example-synergy-app
    ./mvnw spring-boot:run

    2.2 Try to open health check page

    Open http://localhost:8080/health

    3. Add a welcome page

    3.1 Open the generated Maven project in your favourite IDE.

    3.2 Add the HTML page

    The easiest way to create a welcome page is to add an index.html to src/main/resources/static/:

    
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8" />
        <title>Example Synergy app</title>
    </head>
    <body>
        Hello Synergy!
    </body>
    </html>

    3.3 Run/restart the application

    
    ./mvnw spring-boot:run

    3.4 Check welcome page in a browser

    Open http://localhost:8080/

    4. App info endpoint

    Our application’s app-info endpoint should look like this:

    
    {
      "displayName": "Example Synergy app",
      "address": "http://localhost:8080/",
      "identities": [
        { "category": "service", "type": "computation" },
        { "category": "application", "type": "service" }
      ],
      "features": [
        {
          "namespace": "synergy/health",
          "attributes": {
            "url": "http://localhost:8080/health"
          }
        },
        {
          "namespace": "synergy/icon",
          "attributes": {
            "url": "https://www.gravatar.com/avatar/457372368cbaf7fce1427ce46fc4b199?s=64&d=identicon"
          }
        },
        {
          "namespace": "synergy/logout",
          "attributes": {
            "url": "https://morning-brook-95472.herokuapp.com/front-channel-logout"
          }
        },
        {
          "namespace": "producer-service",
          "attributes": {
            "url": "http://localhost:8080/produce"
          }
        }
      ]
    }

    We define the following features here:

    • The health endpoint that Synergy uses to check if your application is running fine

    • The icon that is used in Synergy for your application

    • The logout endpoint that Synergy calls when a user logs out from Synergy (we will implement this later)

    • A service called producer-service , that is a sample for a custom service provided by your application (we will implement this later)

    To learn more about these features, check the Synergy Feature Catalogue.

    4.1 Create class AppInfoController

    The class should be placed in the com.example package so that Spring Boot finds it automatically.

    
    package com.example;
    
    import java.security.MessageDigest;
    import java.util.Arrays;
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.Optional;
    import java.util.function.Supplier;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.xml.bind.DatatypeConverter;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
    import org.springframework.web.util.UriComponentsBuilder;
    
    @Controller
    public class AppInfoController {
    
        private static final Logger LOG = LoggerFactory.getLogger(AppInfoController.class);
    
        @Value("${producer.name:#{null}}")
        private String producerName;
    
        @RequestMapping("/app-info")
        @ResponseBody
        Object appInfo(final HttpServletRequest request) throws Exception {
            Supplier<UriComponentsBuilder> uriBuilder =
                () -> ServletUriComponentsBuilder.fromContextPath(request) ;
    
            // TODO: give the application some nice name
            String applicationDisplayName = "Example Synergy app";
    
            Map<String, Object> info = new LinkedHashMap<>();
            info.put("displayName", applicationDisplayName);
            String address = uriBuilder.get().replacePath("/").toUriString();
            info.put("address", address);
    
            info.put("identities", Arrays.asList(
                    identity("service", "computation"),
                    identity("application", "service")));
    
            // generate a unique hash for the application
            String hash = DatatypeConverter.printHexBinary(
                    MessageDigest.getInstance("md5").digest(address.getBytes())).toLowerCase();
    
            // TODO: give the producer service some custom unique name
            String producerServiceName = Optional.ofNullable(producerName).orElse(hash + "-producer");
    
            info.put("features", Arrays.asList(
               feature("synergy/health",
                   uriBuilder.get().replacePath("/health").toUriString()),
               feature("synergy/icon",
                   String.format("https://www.gravatar.com/avatar/%s?s=64&d=identicon", hash)),
               feature("synergy/logout",
                   uriBuilder.get().replacePath("/front-channel-logout").toUriString()),
               feature(producerServiceName,
                   uriBuilder.get().replacePath("/produce").toUriString())));
    
            return info;
        }
    
        /** Helper methods for assembling app-info json **/
    
        private static Map<String, Object> identity(final String category, final String type) {
            Map<String, Object> identity = new LinkedHashMap<>();
            identity.put("category", category);
            identity.put("type", type);
            return identity;
        }
    
        private static Map<String, Object> feature(final String namespace, final String url) {
            Map<String, Object> feature = new LinkedHashMap<>();
            feature.put("namespace", namespace);
            Map<String, Object> attributes = new LinkedHashMap<>();
            attributes.put("url", url);
            feature.put("attributes", attributes);
            return feature;
        }
    
    }

    4.2 Run/restart the application

    
    ./mvnw spring-boot:run

    4.3 Check application info in a browser

    Open http://localhost:8080/app-info

    5. Deploy to Heroku

    5.1 Create a Git repo and commit your sources

    
    git init
    git add .
    git commit -m "first commit"

    5.2 Create Heroku app and deploy the application

    
    heroku login
    heroku create
    git push heroku master
    heroku open

    6. Register app into Synergy

    If you send us your deployment’s application info URL, we’ll register it for you.

    7. Implement authentication

    images/download/attachments/1806875/synergy-sso-direct.png

    7.1 Add security dependencies

    Add new dependencies in your pom.xml :

    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-jwt</artifactId>
    </dependency>

    7.2 Enable Oauth2 SSO for Spring Boot app

    It can be done by adding @EnableOAuth2Sso annotation.

    {primary} Security must be disabled on health and app-info endpoints.

    Create class SynergySecurityConfiguration, extend WebSecurityConfigurerAdapter and override configure method:

    
    package com.example;
    
    import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    
    @Configuration
    @EnableOAuth2Sso
    public class SynergySecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Override
        public void configure(final WebSecurity web) throws Exception {
            web.ignoring().antMatchers(
                    "/app-info",
                    "/health");
        }
    }

    7.3 Set configuration required by OAuth2

    Rename empty config file application.properties to application.yml .

    Add these to application.yml :

    
    synergy:
      url: https://team1.synergy-dev.cxcloud.io
    security:
      oauth2:
        client:
          clientId: [your app's client id]
          clientSecret: [your app's client secret]
          accessTokenUri: ${synergy.url}/oauth/token
          userAuthorizationUri: ${synergy.url}/oauth/authorize
        resource:
          jwt.key-uri: ${synergy.url}/public/publickey/ssh-rsa.json

    {info} Ask for your client id and client secret!

    7.4 Commit & Deploy

    
    git add .
    git commit -m "configure authentication"
    git push heroku master

    7.5 Open your app directly on Heroku

    
    heroku open

    There should be a redirect to authenticate you when opening the application. (You might not notice it, when already logged in to Synergy.)

    8. Print user info

    The logged in user's info is contained in the JWT access token provided by Synergy. To demonstrate how it can be accessed, we will print it on the welcome page.

    8.1 Extract custom token data

    Create class SynergyAccessTokenConfigurer and implement JwtAccessTokenConverterConfigurer :

    
    package com.example;
    
    import java.util.Collection;
    import java.util.Collections;
    import java.util.Map;
    
    import org.springframework.boot.autoconfigure.security.oauth2.resource.JwtAccessTokenConverterConfigurer;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
    import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
    import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
    import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
    
    @Configuration
    public class SynergyAccessTokenConfigurer implements JwtAccessTokenConverterConfigurer {
    
        @Bean
        UserAuthenticationConverter userAuthenticationConverter() {
            return new DefaultUserAuthenticationConverter() {
                @Override
                public Authentication extractAuthentication(final Map<String, ?> token) {
                    Collection<? extends GrantedAuthority> authorities = Collections.emptyList();
                    // Use whole token as user principal
                    return new UsernamePasswordAuthenticationToken(token, "N/A", authorities);
                }
            };
        }
    
        @Override
        public void configure(final JwtAccessTokenConverter converter) {
            DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
            accessTokenConverter.setUserTokenConverter(userAuthenticationConverter());
            converter.setAccessTokenConverter(accessTokenConverter);
        }
    
    }

    8.2 Add the user info to the welcome page

    In order to do this, we will convert index.html to a template. We will use Thymeleaf as our template engine, so you need to add the following dependency to your pom.xml:

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

    In order to convert index.html to a template, we need to move it from src/main/resources/static/ to src/main/resources/templates/. Once it's a template, we can add the user info:

    
    <!DOCTYPE html>
    <html xmlns:th="https://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8" />
        <title>Example Synergy app</title>
    </head>
    <body>
        <h2>User info from your access token:</h2>
        <table>
            <tr th:each="entry : ${user}">
                <td th:text="${entry.key}"></td>
                <td th:text="${entry.value}"></td>
            </tr>
        </table>
    </body>
    </html>

    We also need to create a controller that uses the template, let's call it IndexPageController:

    
    package com.example;
    
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class IndexPageController {
    
        @RequestMapping("/")
        String indexPage(Model model) {
            model.addAttribute("user", SecurityContextHolder.getContext().getAuthentication().getPrincipal());
            return "index";
        }
    }

    8.3 Commit & Deploy

    
    git add .
    git commit -m "print userinfo"
    git push heroku master

    When opening your application, you should see the user info contained in the access token from Synergy.

    9. Implement authentication of rest endpoints

    Public rest enpoints must understand Synergy tokens sent in a http header.

    9.1 Create resource server configuration

    Create new class ResourceServerConfiguration :

    
    package com.example;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
    import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
    import org.springframework.security.web.util.matcher.RequestMatcher;
    
    @Configuration
    @EnableResourceServer
    public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    
        @Bean("resourceServerRequestMatcher")
        public RequestMatcher resources() {
            return new RequestHeaderRequestMatcher("Authorization");
        }
    
        @Override
        public void configure(final HttpSecurity http) throws Exception {
            http
                .requestMatcher(resources()).authorizeRequests()
                .anyRequest().authenticated();
        }
    
        @Override
        public void configure(final ResourceServerSecurityConfigurer resources) throws Exception {
            // we accept tokens for any resourceId
            resources.resourceId(null);
        }
    
    }

    Notice here we have created a RequestMatcher to separate API calls. For now it is matched when request contains an Authorization header (which should contain the token).

    9.2 Modify SynergySecurityConfiguration

    
    package com.example;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
    import org.springframework.security.web.util.matcher.RequestMatcher;
    
    @Configuration
    @EnableOAuth2Sso
    public class SynergySecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Autowired
        @Qualifier("resourceServerRequestMatcher")
        private RequestMatcher resources;
    
        @Override
        protected void configure(final HttpSecurity http) throws Exception {
            RequestMatcher nonResources = new NegatedRequestMatcher(resources);
            http
                .requestMatcher(nonResources)
                .authorizeRequests().anyRequest().authenticated();
        }
    
        @Override
        public void configure(final WebSecurity web) throws Exception {
            web.ignoring()
                .antMatchers(
                    "/app-info",
                    "/health");
        }
    
    }

    9.3 Commit & Deploy

    
    git add .
    git commit -m "authentication of rest endpoints"
    git push heroku master

    Now SSO is only configured for requests which are not handled by the resource server configuration.

    10. Logout

    We will discuss two forms of logout. In the first case the logout will be initiated from your application, in the second case it will be initiated from Synergy.

    10.1 Logout initiated from your application

    We suggest logging users out of Synergy too when they log out of your application. This allows users to log in as a different user after they log out from your application. In order to implement this, you need to redirect users to Synergy's logout URL after your application has successfully logged them out. That can be set in SynergySecurityConfiguration:

    
    package com.example;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
    import org.springframework.security.web.util.matcher.RequestMatcher;
    
    @Configuration
    @EnableOAuth2Sso
    public class SynergySecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        @Autowired
        @Qualifier("resourceServerRequestMatcher")
        private RequestMatcher resources;
    
        @Value("${synergy.url}/logout")
        private String synergyLogoutUrl;
    
        @Override
        protected void configure(final HttpSecurity http) throws Exception {
            RequestMatcher nonResources = new NegatedRequestMatcher(resources);
            http
                    .requestMatcher(nonResources)
                    .authorizeRequests().anyRequest().authenticated().and()
                    .logout().logoutSuccessUrl(synergyLogoutUrl);
        }
    
        @Override
        public void configure(final WebSecurity web) throws Exception {
            web.ignoring()
                    .antMatchers(
                            "/app-info",
                            "/health");
        }
    
    }

    To add a logout button to the welcome page, change index.html:

    
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8" />
        <title>Example Synergy app</title>
    </head>
    <body>
        <h2>User info from your access token:</h2>
        <table>
            <tr th:each="entry : ${user}">
                <td th:text="${entry.key}"></td>
                <td th:text="${entry.value}"></td>
            </tr>
        </table>
        <form th:action="@{/logout}" method="post">
            <input type="submit" value="Log out" />
        </form>
    </body>
    </html>

    Pressing this logout button will log you out from both your application and Synergy.

    10.2 Logout initiated from Synergy

    When users log out from Synergy, they expect they will be logged out from the connected applications too. This can be achieved for your application by implementing the synergy/logout feature that we already registered with the application info above. This requires an endpoint for front-channel logout, which we will configure in a new class called FrontChannelLogoutConfig:

    
    package com.example;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.annotation.Order;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
    
    @Configuration
    @Order(1)
    public class FrontChannelLogoutConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(final HttpSecurity http) throws Exception {
            HttpStatusReturningLogoutSuccessHandler logoutSuccessHandler = new HttpStatusReturningLogoutSuccessHandler();
            http
                    .antMatcher("/front-channel-logout")
                    .logout().logoutUrl("/front-channel-logout").logoutSuccessHandler(logoutSuccessHandler).and()
                    .csrf().disable();
        }
    }

    10.3 Commit & Deploy

    
    git add .
    git commit -m "logout"
    git push heroku master

    If you now log out from either your application or Synergy, you will be logged out from both.

    11. Demonstrate application to application communication

    11.1 Create ServiceController and implement a producer service

    
    package com.example;
    
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.Random;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    @Controller
    public class ServiceController {
    
        private static final Logger LOG = LoggerFactory.getLogger(ServiceController.class);
    
        @RequestMapping("/produce")
        @ResponseBody
        Object produce(final HttpServletRequest request) throws Exception {
            LOG.info("produce called by: {}", SecurityContextHolder.getContext().getAuthentication());
    
            Map<String, Object> result = new LinkedHashMap<>();
            // TODO: add your custom data or computation to the result
            result.put("data", new Random().nextInt());
            result.put("producedBy", request.getRequestURL());
            return result;
        }
    }

    11.2 Create helper class for obtaining token for your application

    
    package com.example;
    
    import java.util.Arrays;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
    import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
    import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
    import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
    import org.springframework.security.oauth2.common.OAuth2AccessToken;
    import org.springframework.stereotype.Component;
    
    @Component
    public class ClientCredentialsTokenProvider {
    
        private final ClientCredentialsAccessTokenProvider tokenProvider = new ClientCredentialsAccessTokenProvider();
    
        private final String accessTokenUri;
        private final String clientId;
        private final String clientSecret;
    
        @Autowired
        public ClientCredentialsTokenProvider(final OAuth2ProtectedResourceDetails resourceDetails) {
            this(
                resourceDetails.getAccessTokenUri(),
                resourceDetails.getClientId(),
                resourceDetails.getClientSecret());
        }
    
        public ClientCredentialsTokenProvider(
                final String accessTokenUri,
                final String clientId,
                final String clientSecret) {
            this.accessTokenUri = accessTokenUri;
            this.clientId = clientId;
            this.clientSecret = clientSecret;
        }
    
        public OAuth2AccessToken getToken(final String... scopes) {
            ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
            resourceDetails.setAccessTokenUri(accessTokenUri);
            resourceDetails.setClientId(clientId);
            resourceDetails.setClientSecret(clientSecret);
            resourceDetails.setScope(Arrays.asList(scopes));
            return tokenProvider.obtainAccessToken(resourceDetails, new DefaultAccessTokenRequest());
        }
    }

    11.3 Create RestTemplateConfiguration

    It configures a rest service client to forward the current OAuth2 token in Authorization header of each request.

    
    package com.example;
    
    import java.util.Optional;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.oauth2.provider.OAuth2Authentication;
    import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
    import org.springframework.web.client.RestTemplate;
    
    @Configuration
    public class RestTemplateConfiguration {
    
        @Autowired
        private ClientCredentialsTokenProvider tokenProvider;
    
        @Bean
        public RestTemplate restTemplate() {
            RestTemplate template = new RestTemplate();
            template.getInterceptors().add((request, body, execution) -> {
                final String token;
                Optional<OAuth2AuthenticationDetails> currentOAuth2Details = currentOAuth2Details();
                if (currentOAuth2Details.isPresent()) {
                    // get token from current security context
                    token = currentOAuth2Details.get().getTokenValue();
                } else {
                    // request a new token as an application (server-to-server communication)
                    token = tokenProvider.getToken("read").getValue();
                }
    
                // forward token in Authorization header
                request.getHeaders().add("Authorization", "Bearer " + token);
    
                return execution.execute(request, body);
            });
            return template;
        }
    
        private static Optional<OAuth2AuthenticationDetails> currentOAuth2Details() {
            return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
                .filter(OAuth2Authentication.class::isInstance)
                .map(OAuth2Authentication.class::cast)
                .map(OAuth2Authentication::getDetails)
                .map(OAuth2AuthenticationDetails.class::cast);
        }
    
    }

    11.4 Create DiscoveryClient

    It’s a client for Synergy service discovery.

    
    package com.example;
    
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.Optional;
    import java.util.stream.StreamSupport;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    import org.springframework.web.client.RestTemplate;
    
    import com.fasterxml.jackson.databind.JsonNode;
    
    @Component
    public class DiscoveryClient {
    
        @Autowired
        private RestTemplate restTemplate;
    
        @Value("${synergy.url}/api/discover")
        private String discoveryUrl;
    
        public String getFeatureUrl(final String namespace) {
            Map<String, String> all = findFeature(namespace);
            if (all.isEmpty()) {
                throw new IllegalArgumentException("No applications found with feature " + namespace);
            }
            if (all.size() > 1) {
                throw new IllegalArgumentException("Multiple applications found with feature " + namespace + ": " + all.keySet());
            }
            return all.entrySet().iterator().next().getValue();
        }
    
        private Map<String, String> findFeature(final String namespace) {
            Map<String, String> result = new LinkedHashMap<>();
            for (JsonNode application : restTemplate.getForObject(discoveryUrl, JsonNode.class)) {
                Optional<String> featureUrl = StreamSupport.stream(application.get("features").spliterator(), false)
                        .filter(feature -> feature.get("namespace").asText().equals(namespace))
                        .map(feature -> feature.get("attributes").get("url").asText())
                        .findFirst();
                if (featureUrl.isPresent()) {
                    result.put(application.get("displayName").asText(), featureUrl.get());
                }
            }
            return result;
        }
    }

    11.5 Create App2AppCommunication

    It contains an 2 endpoint for consuming another application’s producer service:

    • one using the token of the logged in user /consume

    • one is requesting a new token in the name of your application (server-to-server communication) /consume-as-app

    
    package com.example;
    
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.concurrent.FutureTask;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.client.RestTemplate;
    
    import com.fasterxml.jackson.databind.JsonNode;
    
    @RestController
    public class App2AppCommunication {
    
        private static final Logger LOG = LoggerFactory.getLogger(App2AppCommunication.class);
    
        @Autowired
        private RestTemplate template;
    
        @Autowired
        private DiscoveryClient discovery;
    
        @RequestMapping("/consume/{namespace}")
        public Object consumeAsUser(@PathVariable("namespace") final String namespace, final HttpServletRequest request) throws Exception {
            LOG.info("consumeAsUser called by: {}", SecurityContextHolder.getContext().getAuthentication().getPrincipal());
            return consume(namespace, request);
        }
    
        @RequestMapping("/consume-as-app/{namespace}")
        public Object consumeAsApp(@PathVariable("namespace") final String namespace, final HttpServletRequest request) throws Exception {
            LOG.info("consumeAsApp called by: {}", SecurityContextHolder.getContext().getAuthentication().getPrincipal());
            // call consume on other thread (without security context)
            FutureTask<Object> task = new FutureTask<>(() -> consume(namespace, request));
            new Thread(task).start();
            return task.get();
        }
    
        private Object consume(final String namespace, final HttpServletRequest request) throws Exception {
            // discover remote service
            String remoteServiceUrl = discovery.getFeatureUrl(namespace);
    
            // call remote service
            JsonNode producerResult
                = template.getForObject(remoteServiceUrl, JsonNode.class);
    
            Map<String, Object> result = new LinkedHashMap<>();
            result.put("producerResult", producerResult);
            result.put("consumedBy", request.getRequestURL());
            return result;
        }
    
    }

    11.6 Commit & Deploy

    
    git add .
    git commit -m "app2app communication"
    git push heroku master

    11.7 Open your app directly on Heroku, ask for name of another service (or use yours)

    
    heroku open /consume/{name-of-another-service}
    
    heroku open /consume-as-app/{name-of-another-service}

    11.8 Display application logs to check communication

    
    heroku logs -t