By default, Symphony 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
Chat Bot: Listens to commands from the user and contacts the other components
Authorization Server: A server that integrates with the Identity Provider. This server is trusted with privileged access to Symphony REST APIs.
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.
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 events.
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.
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.
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.
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")publicStringgetToken(@RequestBodyMessageIdentityRequest 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())) {thrownewResponseStatusException(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()) {thrownewResponseStatusException(BAD_REQUEST,"Unable to determine subject"); }String username =users.get(0).getEmailAddress();String subject =matcher.group(0);returngenerateJwt(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:
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.
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.
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
@ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter { @Value("${resource-server.publicKeyUri}")privateString publicKeyUri; @Overrideprotectedvoidconfigure(HttpSecurity http) throwsException {// Fetch public key from Authorization ServerHttpRequest request =HttpRequest.newBuilder().uri(newURI(publicKeyUri)).build();String response =HttpClient.newBuilder().build().send(request,HttpResponse.BodyHandlers.ofString()).body();// Load public keyString 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(newX509EncodedKeySpec(keyBytes));// Configure all endpoints to be authenticated// and use this auth filter in the chainhttp.authorizeRequests().anyRequest().authenticated().and().addFilter(newJWTAuthorizationFilter(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
@RestControllerpublicclassBalanceController { @GetMapping("/api/balance")publiclonggetBalance(UsernamePasswordAuthenticationToken token) {String grant =token.getCredentials().toString();if (grant.equals("/balance")) {// in reality: fetch the user's actual balancereturn (long) (Math.random() *777); }else {thrownewResponseStatusException(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.
Copy the MessageIdentityRequest class from the Authorization Server project into this project. Then, create 2 feign clients to communicate with the Authorization and Resource Server endpoints.
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
@ComponentpublicclassBalanceCommandHandler {privatefinalLogger log =LoggerFactory.getLogger(this.getClass());privatefinalMessageService messageService;privatefinalMessageIdentityClient messageIdentityClient;privatefinalBalanceClient balanceClient;publicBalanceCommandHandler(MessageService messageService,MessageIdentityClient messageIdentityClient,BalanceClient balanceClient ) {this.messageService= messageService;this.messageIdentityClient= messageIdentityClient;this.balanceClient= balanceClient; } @Slash(value ="/balance", mentionBot =false)publicvoidshowBalance(CommandContext context) {log.info("Balance requested");String messageId =context.getMessageId();String username =context.getInitiator().getUser().getEmail();try {// Get token from auth serverMessageIdentityRequest request =newMessageIdentityRequest(messageId, username);String token =messageIdentityClient.getToken(request);// Fetch data from resource serverString 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 usermessageService.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.