Page tree

Table of Contents

1. Prerequisites

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

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.

 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

 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