Table of Contents
Your favourite Java IDE
Spring Boot CLI: https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started-installing-spring-boot.html#getting-started-installing-the-cli
Git client
Heroku account: https://signup.heroku.com/
Heroku CLI: https://devcenter.heroku.com/articles/heroku-cli
CXN Pass account: https://pass.chemaxon.com/
{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
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
Open http://localhost:8080/health
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>
./mvnw spring-boot:run
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.
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;
}
}
./mvnw spring-boot:run
Open http://localhost:8080/app-info
git init
git add .
git commit -m "first commit"
heroku login
heroku create
git push heroku master
heroku open
If you send us your deployment’s application info URL, we’ll register it for you.
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>
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");
}
}
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!
git add .
git commit -m "configure authentication"
git push heroku master
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.)
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.
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);
}
}
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";
}
}
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.
Public rest enpoints must understand Synergy tokens sent in a http header.
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).
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");
}
}
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.
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.
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.
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();
}
}
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.
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;
}
}
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());
}
}
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);
}
}
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;
}
}
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;
}
}
git add .
git commit -m "app2app communication"
git push heroku master
heroku open /consume/{name-of-another-service}
heroku open /consume-as-app/{name-of-another-service}
heroku logs -t