Symphony Messaging Dev Docs
Developer CertificationREST API
  • Start Your Developer Journey
  • Bots
    • Building Bots
    • Planning Your Bot
      • Conversational Bot
      • Interactive Bot
      • Headless Bot
    • Getting Started
      • Getting Started with BDK
      • Creating a Service Account
      • Configuration
      • Truststores
    • Overview of REST API
      • REST API Architecture
      • Overview of Pod API
      • Overview of Key Manager API
      • Overview of Agent API
      • Bot Permissions
      • REST API Reference
    • Authentication
      • RSA Authentication Workflow
      • Certificate Authentication Workflow
    • Datafeed
      • Real-Time Events
      • Overview of Streams
    • Messages
      • MessageML
        • MessageML Basics
          • Content Grouping
          • Text formatting and semantics
          • Tables
          • Images
          • Tags and mentions
            • Enhanced tags notice
          • Style Attributes
          • Special Characters
          • Emojis
        • Elements Interactive Forms
          • Buttons
            • Icon set for Buttons
          • Text Field
          • Masked Text Field
          • Text Area
          • Checkbox
          • Radio Button
          • Dropdown Menu
          • Person Selector
          • Room Selector
          • Date Picker
          • Time Picker
          • Timezone Picker
          • Table Select
          • Regular Expressions - Regex
        • Extensibility UI Actions
          • OpenChat
          • Dialog
        • Entities
          • Standard Entities
          • Custom Entities
      • PresentationML
    • Bots Best Practices
    • Open Source Code Samples
  • Extension Apps
    • Building Extension Apps
    • Planning Your App
    • Getting Started
      • Getting Started with ADK
      • Application Manifest Bundle
      • Add an Extension App to a Symphony Pod
    • Overview of Extension API
      • Initialization
      • Register and Connect
      • Extension API Services
        • Service Interface
        • Register and Subscribe
        • Modules Service
        • Entity Service
          • Entity Advanced Templating
          • Message Format - ExtensionML
        • Applications-Nav Service
        • Share Service
        • Commerce Service
        • Dialogs Service
        • UI Service
          • Receiving Conversation and User Information
          • Filter Function
    • App Authentication
      • Circle of Trust Authentication
      • OBO Authentication
  • Developer Tools
    • Symphony Messaging Generator
    • Bot Developer Kit for Java
      • Build a Conversational Bot
      • Build an Interactive Bot
      • Build a Headless Bot
      • Integrate a Bot with an Identity Provider
    • Bot Developer Kit for Python
    • App Developer Kit
      • Build a Basic Extension App
      • Build an Extension App with App Views
        • Add Buttons and Handlers to an Extension App
        • Add BDK to an Extension App for Circle of Trust
      • Build an Extension App with Message Renderers
    • Postman
    • UI Style Guide
      • Colors
      • Form Elements
      • Buttons
  • Embedded Modules
    • Symphony Messaging URI
      • Symphony Messaging URI for Mobile (deprecated)
    • Desktop interoperability
      • FDC3 intents
        • Message format
      • Configuration guide
        • Configure Interop.io
        • Configure Here Core
        • Configure Finsemble
        • Configure with Embedded Mode
        • Troubleshooting
      • Change log
    • Embedded Mode
      • Get started
      • Configuration parameters
      • Open a chat
      • Send a message
      • Create a room
      • Pin a message
      • Notifications
      • Support for extension applications
      • Open an app
      • Embedded Mode with Sponsored Access
      • Pricing tiers
      • Logout
    • Universal Webhook
      • User guide
        • Example with Splunk
      • Installation guide
  • Symphony REST API
    • Messaging REST API
    • Federation
    • Sponsored Access API
    • Enhanced Directory API
  • Developer Certification
    • Developer Certification
  • Mobile Frameworks
    • Blackberry
    • MobileIron
  • Admin Guides
    • Change Logs
      • API Agent
        • Agent - 25.3 (LTS)
        • Agent - 24.12 (LTS)
        • Agent - 24.9 (LTS)
        • Agent - 24.6 (LTS)
        • Archives
          • Agent - 24.11
          • Agent - 24.10
          • Agent - 23.9 (LTS)
          • Agent - 24.8
          • Agent - 24.3 (LTS)
          • Agent - 24.2
          • Agent - 24.1
          • Agent - 23.12 (LTS)
          • Agent - 23.11
          • Agent - 23.10
          • Agent - 23.7
          • Agent - 23.6 (LTS)
          • Agent - 23.4
          • Agent - 23.3 (LTS)
          • Agent - 23.1
          • Agent - 22.12 (LTS)
          • Agent - 22.11
          • Agent - 22.10
          • Agent - 22.9 (LTS)
          • Agent - 22.8
          • Agent - 22.7
          • Agent - 22.6 (LTS)
          • Agent - 20.14
          • Agent - 20.13
          • Agent - 20.12
          • Agent - 20.10
          • Agent - 20.9 (2.62)
          • Agent - 20.7 (2.61)
          • Agent - 20.6 (2.60)
          • Agent - 20.5 (2.59)
          • Agent - 20.4 (2.58)
      • SBE (Pod API)
        • SBE - 24.1
        • SBE - 20.16
        • SBE - 20.15
        • Archives
          • SBE - 20.14
          • SBE - 20.13
          • SBE - 20.12
          • SBE - 20.10
          • SBE - 20.9 (1.62)
          • SBE - 20.7 (1.61)
          • SBE - 20.6 (1.60)
          • SBE - 20.5 (1.59)
          • SBE - 20.4 (1.58)
      • Client 2.0 APIs
        • Client 2.0 - 25.05
        • Client 2.0 - 25.03
        • Client 2.0 - 24.12
        • Client 2.0 - 24.05
        • Client 2.0 - 23.02
        • Client 2.0 - 22.11
        • Archives
          • Client 2.0 - 20.4
          • Client 2.0 - 20.5
          • Client 2.0 - 20.6
          • Client 2.0 - 20.7
          • Client 2.0 - 20.9
          • Client 2.0 - 20.10
          • Client 2.0 - 20.12
          • Client 2.0 - 22.8
          • Client 2.0 - 22.10
      • Universal Webhook
        • Univ Webhook - 2.6
        • Univ Webhook - 2.4
        • Univ Webhook - 2.2
        • Univ Webhook - 2.1
        • Univ Webhook - 2.0
    • API Change Management
    • Global Throttling
    • Agent Guide
      • Network Topology
      • Agent Download
      • Agent Installation
      • Agent Configuration Fields
      • Agent Server High Availability
      • Agent Performance Tuning
Powered by GitBook
On this page
  • Overview
  • Components and Flow
  • Create Authorization Server
  • Configure Authorization Server
  • Create Message Identity Request Object
  • Create Message Identity Service
  • Create Resource Server
  • Configure Resource Server
  • Configure Spring Security
  • Add Resource Endpoint
  • Create Bot
  • Configure Bot
  • Add HTTP Clients
  • Add Command Handler
  • Result
  • Source Code

Was this helpful?

Export as PDF
  1. Developer Tools
  2. Bot Developer Kit for Java

Integrate a Bot with an Identity Provider

Last updated 1 month ago

Was this helpful?

Overview

By default, Symphony Messaging provides secure authentication and authorization for all users. However, if you already have an identity provider that authenticates and authorizes users for resources and you want those resources served by a bot, you will need to implement this integration yourself. This tutorial seeks to demonstrate one such implementation using an OAuth-like flow.

Components and Flow

  1. Chat Bot: Listens to commands from the user and contacts the other components

  2. Authorization Server: A server that integrates with the Identity Provider. This server is trusted with privileged access to Symphony Messaging REST APIs.

  3. Resource Server: A server where requested resources are served from. This server does not trust the bot to make arbitrary calls for user-owned data.

This sequence diagram describes the flow of requests between the various components.

Create Authorization Server

We will be using the BDK for this example, so generate a scaffold project using the Bot Generator by following these instructions. We will also be using the Spring Boot integration so ensure that is selected at the framework question in the generator.

Once you have your project, we will first add web capability by changing spring-boot-starter to spring-boot-starter-web, then adding com.auth0:java-jwt for minting tokens.

pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.symphony.platformsolutions</groupId>
        <artifactId>symphony-bdk-core-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.12.0</version>
    </dependency>
</dependencies>

Configure Authorization Server

If you haven't already configured your bot in application.yaml, do that now. Then, generate a new RSA key pair used to sign and verify tokens.

openssl genrsa -out sign-private.pem 2048
openssl rsa -in sign-private.pem -outform PEM -pubout -out sign-public.pem

Add the keys to your project and list their paths in your application.yaml file. We will also disable the BDK's datafeed since this server will initiate calls from HTTP endpoints rather than Symphony Messaging events.

application.yaml
bdk:
  host: develop2.symphony.com
  bot:
    username: bot-username
    privateKey:
      path: auth-server/rsa/auth-bot-private.pem
  datafeed:
    enabled: false

auth-server:
  privateKey: auth-server/rsa/sign-private.pem
  publicKey: auth-server/rsa/sign-public.pem

Create Message Identity Request Object

We will use this object to request for tokens from the Authorization Server, where we want to validate if a message ID is valid and the initiator's user ID matches what was requested.

MessageIdentityRequest.java
public class MessageIdentityRequest {
  private String messageId;
  private String username;
  
  // Constructors, Getters, Setters
}

Create Message Identity Service

This service will be our main logic class, serving HTTP requests and processing validation. We will inject in the location of our keys and use the BDK message service and user service.

MessageeIdentityService.java
@RestController
public class MessageIdentityService {
  @Value("${auth-server.privateKey}")
  private String privateKeyPath;

  @Value("${auth-server.publicKey}")
  private String publicKeyPath;
  
  private final MessageService messageService;
  private final UserService userService;

  public MessageIdentityService(MessageService messageService, UserService userService) {
    this.messageService = messageService;
    this.userService = userService;
  }
}

We'll then add some utilities for loading our keys from disk.

private Key readKey(String filePath, boolean isPrivate) throws Exception {
  String keyString = Files.readString(Paths.get(filePath))
    .replaceAll("-----(BEGIN|END) (PUBLIC|PRIVATE) KEY-----", "")
    .replaceAll(System.lineSeparator(), "");

  byte[] keyBytes = Base64.decodeBase64(keyString);
  KeyFactory keyFactory = KeyFactory.getInstance("RSA");

  if (isPrivate) {
    return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
  }
  return keyFactory.generatePublic(new X509EncodedKeySpec(keyBytes));
}

public RSAPrivateKey readPrivateKey(String filePath) throws Exception {
  return (RSAPrivateKey) readKey(filePath, true);
}

public RSAPublicKey readPublicKey(String filePath) throws Exception {
  return (RSAPublicKey) readKey(filePath, false);
}

Then we'll add a method to mint a JWT token. The contents of the token are arbitrary, but the recipient needs to understand the structure. We will be setting the user's email as the audience and the grant (or permission scope) to the subject.

private String generateJwt(String username, String subject) throws Exception {
  RSAPrivateKey privateKey = readPrivateKey(privateKeyPath);
  RSAPublicKey publicKey = readPublicKey(publicKeyPath);
  Instant now = Instant.now();
  
  return JWT.create()
    .withIssuedAt(Date.from(now))
    .withExpiresAt(Date.from(now.plus(5L, ChronoUnit.MINUTES)))
    .withAudience(username)
    .withSubject(subject)
    .sign(Algorithm.RSA512(publicKey, privateKey));
}

Next, we'll add an endpoint to serve the public key. This will be used for resource servers to validate any tokens they receive.

@GetMapping("/public-key")
public String getPublicKey() throws Exception {
  log.info("Public key retrieved");
  return Files.readString(Paths.get(publicKeyPath));
}

Finally, the core logic goes into the token request process. This method obtains the original message payload from Symphony and validates that the actual sender matches the request. It then goes ahead to extract the intent of the message - assuming that this is for command-based flows where the first non-@mention word is the intent. That word will be used as the grant. The user's email address and grant are then used to mint a JWT token and it is returned.

@PostMapping("/token")
public String getToken(@RequestBody MessageIdentityRequest request) throws Exception {
  V4Message message = messageService.getMessage(request.getMessageId());
  List<UserV2> users = userService.listUsersByEmails(List.of(request.getUsername()));

  if (!message.getUser().getUserId().equals(users.get(0).getId())) {
    throw new ResponseStatusException(BAD_REQUEST, "Message did not originate from user");
  }

  String msgText = message.getMessage()
    .replaceAll("<[^>]*>", "");
  Pattern pattern = Pattern.compile("(?<=((^|\\s)/?))(?:(?!@)[^\\s])+(?=($|\\s))");
  Matcher matcher = pattern.matcher(msgText);

  if (!matcher.find()) {
    throw new ResponseStatusException(BAD_REQUEST, "Unable to determine subject");
  }

  String username = users.get(0).getEmailAddress();
  String subject = matcher.group(0);
  return generateJwt(username, subject);
}

Create Resource Server

The resource server will be a standard Spring Boot web server, enabled with Spring Security and will have no knowledge of Symphony. Create an empty maven project and add these dependencies:

pom.xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.1</version>
    <relativePath/>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.12.0</version>
    </dependency>
</dependencies>

Configure Resource Server

Add an application.yaml to your resources root and add in the URI to obtain the public key on the Authorization Server we added earlier. We will also change the port to 8081 so that it can run concurrently with the Authorization Server that was left defaulted to 8080.

application.yaml
server:
  port: 8081

resource-server:
  publicKeyUri: http://localhost:8080/public-key

This project will use a standard Spring Boot main class as follows.

ResourceServer.java
@SpringBootApplication
public class ResourceServer {
  public static void main(String[] args) {
    SpringApplication.run(ResourceServer.class, args);
  }
}

Configure Spring Security

Next, add a Spring Security filter to pre-process incoming requests on the Resource Server. This filter first extracts the JWT token from the Authorization header, verifies the signature using the Authorization Server's public key, then adds the the user and grant to the security context's Authentication for use in controllers.

JWTAuthorizationFilter.java
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
  private final Logger log = LoggerFactory.getLogger(this.getClass());
  public static final String HEADER_STRING = "Authorization";
  public static final String TOKEN_PREFIX = "Bearer ";
  public final RSAPublicKey publicKey;

  public JWTAuthorizationFilter(AuthenticationManager authManager, RSAPublicKey publicKey) {
    super(authManager);
    this.publicKey = publicKey;
  }

  @Override
  protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) {
    log.info("Authorising");
    String token = req.getHeader(HEADER_STRING);

    try {
      if (token != null && token.startsWith(TOKEN_PREFIX)) {
        DecodedJWT jwt = JWT.require(Algorithm.RSA512(publicKey, null))
          .build()
          .verify(token.replace(TOKEN_PREFIX, ""));

        String subject = jwt.getSubject();
        String audience = jwt.getAudience().get(0);
        
        // In reality: check that this user exists

        log.info("Authorised: [{}] {}", audience, subject);

        SecurityContextHolder.getContext().setAuthentication(
          new UsernamePasswordAuthenticationToken(audience, subject, List.of()));
      }
      chain.doFilter(req, res);
    } catch (Exception e) {
      log.error("Authorisation failed");
    }
  }
}

This filter then needs to be added to the Spring Security configuration. Before that happens, the public key is fetched from the Authorization Server and loaded.

SecurityConfig.java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  @Value("${resource-server.publicKeyUri}")
  private String publicKeyUri;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // Fetch public key from Authorization Server
    HttpRequest request = HttpRequest.newBuilder()
      .uri(new URI(publicKeyUri)).build();

    String response = HttpClient.newBuilder()
      .build().send(request, HttpResponse.BodyHandlers.ofString()).body();
    
    // Load public key
    String keyString = response
      .replaceAll("-----(BEGIN|END) PUBLIC KEY-----", "")
      .replaceAll(System.lineSeparator(), "");

    byte[] keyBytes = Base64.decodeBase64(keyString);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(keyBytes));
    
    // Configure all endpoints to be authenticated
    // and use this auth filter in the chain
    http.authorizeRequests()
      .anyRequest().authenticated()
      .and()
      .addFilter(new JWTAuthorizationFilter(authenticationManager(), publicKey));
  }
}

Add Resource Endpoint

Finally, the actual resource is served on a standard controller. This controller does a further check against the grant to ensure it aligns with the resource being requested.

BalanceController.java
@RestController
public class BalanceController {
  @GetMapping("/api/balance")
  public long getBalance(UsernamePasswordAuthenticationToken token) {
    String grant = token.getCredentials().toString();
    if (grant.equals("/balance")) {
      // in reality: fetch the user's actual balance
      return (long) (Math.random() * 777);
    }
    else {
      throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Incorrect grant");
    }
  }
}

Create Bot

We are now ready to build the bot itself. This project is similar to the Authorization Server in that it uses BDK 2.0 with the Spring Boot integration, so repeat that process once more. We will add the openfeign project for calling our HTTP endpoints.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.symphony.platformsolutions</groupId>
        <artifactId>symphony-bdk-core-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
        <version>3.0.0</version>
    </dependency>
</dependencies>

We will need to enable feign on the main class by adding the @EnableFeignClients annotation.

Bot.java
@SpringBootApplication
@EnableFeignClients
public class Bot {
  public static void main(String[] args) {
    SpringApplication.run(Bot.class, args);
  }
}

Configure Bot

Add the bot configuration itself and then the location of our Authorization and Resource servers.

application.yaml
bdk:
  host: develop2.symphony.com
  bot:
    username: bot-username
    privateKey:
      path: bot/rsa/user-bot-private.pem

bot:
  auth-server-uri: http://localhost:8080
  resource-server-uri: http://localhost:8081

Add HTTP Clients

MessageIdentityClient.java
@FeignClient(name = "MessageIdentityClient", url = "${bot.auth-server-uri}")
public interface MessageIdentityClient {
  @PostMapping("/token")
  String getToken(MessageIdentityRequest request);
}
BalanceClient.java
@FeignClient(name = "BalanceClient", url = "${bot.resource-server-uri}")
public interface BalanceClient {
  @GetMapping("/api/balance")
  String getBalance(@RequestHeader("Authorization") String token);
}

Add Command Handler

Finally, we'll add a command handler to receive a /balance command from the user. This will first request for a token from the Authorization Server, then use that token to request for the user's bank balance before returning that value in a message to the user.

BalanceCommandHandler.java
@Component
public class BalanceCommandHandler {
  private final Logger log = LoggerFactory.getLogger(this.getClass());
  private final MessageService messageService;
  private final MessageIdentityClient messageIdentityClient;
  private final BalanceClient balanceClient;

  public BalanceCommandHandler(
    MessageService messageService,
    MessageIdentityClient messageIdentityClient,
    BalanceClient balanceClient
  ) {
    this.messageService = messageService;
    this.messageIdentityClient = messageIdentityClient;
    this.balanceClient = balanceClient;
  }

  @Slash(value = "/balance", mentionBot = false)
  public void showBalance(CommandContext context) {
    log.info("Balance requested");
    String messageId = context.getMessageId();
    String username = context.getInitiator().getUser().getEmail();

    try {
      // Get token from auth server
      MessageIdentityRequest request = new MessageIdentityRequest(messageId, username);
      String token = messageIdentityClient.getToken(request);

      // Fetch data from resource server
      String authToken = "Bearer " + token;
      String balance = balanceClient.getBalance(authToken);
      response = "Your balance is: $" + balance;
    } catch (Exception e) {
      response = "You are unauthorised to use this feature";
    }

    // Respond to user
    messageService.send(context.getStreamId(), response);
  }
}

Result

Launch your project now and go to your Symphony pod, search for your bot and issue a /balance command. If your current user account is authorized by both the Authorization and Resource Servers, you will see an account balance message appear like on the left pane below. If not, you will observe the message on the right pane instead.

Source Code

Copy the class from the Authorization Server project into this project. Then, create 2 feign clients to communicate with the Authorization and Resource Server endpoints.

The complete project source code for this tutorial can be found at the following repository.

Getting Started with BDK
https://github.com/SymphonyPlatformSolutions/symphony-sso-example
MessageIdentityRequest