Click here to Skip to main content
15,886,199 members
Articles / Web Development / Spring
Article

Creating an Incident Management Bot with MSAL, Graph SDK, and Java

Rate me:
Please Sign up or sign in to vote.
2.00/5 (1 vote)
22 Nov 2021CPOL8 min read 3.2K   2  
In this article we’ll see how to integrate Spring with Microsoft Teams to create a simple incident management bot.
Here we’ll create a Spring Boot web app that can create a Teams channel and invite a list of stakeholders. The app will also offer a simple UI that an SRE or other incident commander can use to publish status updates to the channel on their behalf with a single click – making it easy for the incident commander to keep stakeholders up to date without pulling themselves out of their workflow to type chat messages.

This article is a sponsored article. Articles such as these are intended to provide you with information on products and services that we consider useful and of value to developers

Communication is key when critical systems go down. Support teams need to keep customers informed, product owners need to understand the impact of outages, and engineering teams need to coordinate their efforts.

Chat tools like Microsoft Teams provide a natural collaboration point. But unstructured conversation forces stakeholders to read back through pages of chat messages to try and infer the state of any remediation work. Or, it could result in having to interrupt those currently solving the issue with explicit status requests.

An incident management bot provides a solution by allowing a limited number of agreed-upon status messages to be posted to a chat. This allows stakeholders to instantly understand the state of remediation work.

In the first article of this series, we created an authentication provider using the Microsoft Graph SDK for Java. In the second article, we used OneNote to Markdown with Java and the Microsoft Graph SDK. In this third and final article of the series, we’ll create a simple incident management bot with MSAL, Microsoft Teams, the Microsoft Graph API, and Spring Boot.

The Sample Application

The source code for this sample application can be found in the GitHub MSALIncidentManagementDemo repo.

Combine Front-End and Back-End Logic

This sample application exposes the web front-end and OAuth resource server in a single application. This approach is described in the Microsoft documentation.

Bootstrap the Spring Project

We’ll generate the initial application template using Spring Initalizr to create a Java Maven project generating a JAR file using the latest non-snapshot version of Spring against Java 11.

The project — a combined web application and resource server — requires the following dependencies:

Image 1

Configure Spring and Azure AD

Our application.yaml file contains the details of the Azure AD application.

azure:
  activedirectory:
    tenant-id: ${TENANT_ID}
    client-id: ${CLIENT_ID}
    client-secret: ${CLIENT_SECRET}
    app-id-uri: ${APP_ID_URI}
    application-type: web_application_and_resource_server
    authorization-clients:
      api:
        authorizationGrantType: authorization_code
        scopes:
          - api://d21b7691-b12b-4a9a-a35e-542a0a577f78/Teams

The application-type property must be set to web_application_and_resource_server when building a combined web application and resource server.

application-type: web_application_and_resource_server

Also, note that we create an authorized client that grants access back to the same application. This client is how the web application controller communicates with the resource server controller:

authorization-clients:
  api:
    authorizationGrantType: authorization_code
    scopes:
      - api://d21b7691-b12b-4a9a-a35e-542a0a577f78/Teams

Configure Spring Security

We configure Spring Security through the AuthSecurityConfig class, in the following package:

Java
package com.matthewcasperson.incidentmanagementdemo.configuration;

Our security class nests two static classes.

The first is the ApiWebSecurityConfigurationAdapter class, which configures the endpoints exposed by the OAuth resource server. This class extends the AADResourceServerWebSecurityConfigurerAdapter class, overrides the configure method, and calls the base configure method to allow AADResourceServerWebSecurityConfigurerAdapter to initialize common resource server security rules.

Java
@EnableWebSecurity
public class AuthSecurityConfig {
 
  @Order(1)
  @Configuration
  public static class ApiWebSecurityConfigurationAdapter extends
      AADResourceServerWebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) throws Exception {
      super.configure(http);

We then require all requests to endpoints prefixed with /api to be authenticated.

Java
    // All the paths that match `/api/**`(configurable) work as the resource server.
    // Other paths work as the web application.
    // @formatter:off
    http
      .antMatcher("/api/**")
      .authorizeRequests().anyRequest().authenticated();
    // @formatter:on
  }
}

The HtmlWebSecurityConfigurerAdapter class configures the endpoints exposed by the webserver. This class extends the AADWebSecurityConfigurerAdapter class, overrides the configure method, and calls the base configure method to allow AADWebSecurityConfigurerAdapter to initialize common web application security rules.

Java
@Configuration
public static class HtmlWebSecurityConfigurerAdapter extends AADWebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    super.configure(http);

We configure rules to allow static assets like JavaScript and CSS files, as well as the login page, to be requested without authentication. We then require authentication for requests to all other endpoints, and disable Cross Site Request Forgery (CSRF) to simplify the development of the sample application:

JavaScript
      // @formatter:off
      http
        .authorizeRequests()
          .antMatchers("/login", "/*.js", "/*.css").permitAll()
          .anyRequest().authenticated()
        .and()
          .csrf()
          .disable();
      // @formatter:on
    }
  }
}

Create the Graph API Client

We will access the Microsoft Graph API via the official Graph API client. First, we expose an instance of the client as a bean in the GraphClientConfiguration class:

Java
package com.matthewcasperson.incidentmanagementdemo.providers;

We inject an instance of the AADAuthenticationProperties class, providing easy access to the configuration values in our application.yml file.

Java
@Configuration
public class GraphClientConfiguration {
 
  @Autowired
  AADAuthenticationProperties azureAd;

The client is created in the getClient method. We make use of the OboAuthenticationProvider class introduced in a previous article to generate an on-behalf-of (OBO) token suitable for accessing the Graph API. The list of scopes contains the permissions required to list and create channels, assign users to channels, and create messages in channels:

Java
  @Bean
  public GraphServiceClient<Request> getClient() {
    return GraphServiceClient.builder()
        .authenticationProvider(new OboAuthenticationProvider(
            Set.of("https://graph.microsoft.com/Channel.Create",
                "https://graph.microsoft.com/ChannelSettings.Read.All",
                "https://graph.microsoft.com/ChannelMember.ReadWrite.All",
                "https://graph.microsoft.com/ChannelMessage.Send",
                "https://graph.microsoft.com/Team.ReadBasic.All",
                "https://graph.microsoft.com/TeamMember.ReadWrite.All",
                "https://graph.microsoft.com/User.ReadBasic.All"),
            azureAd.getTenantId(),
            azureAd.getClientId(),
            azureAd.getClientSecret()))
        .buildClient();
  }
}

The OBO Token Provider

The OboAuthenticationProvider class was covered in detail in a previous article. The code we use here will be identical, except for the package statement:

Java
package com.matthewcasperson.incidentmanagementdemo.providers;

At a high level, this class extracts the JWT passed to the OAuth resource server endpoints and exchanges it for an OBO token with a given set of scopes.

Create the WebClient

Even though we’re exposing a web application and OAuth resource server with a single Spring Boot application, the webserver accesses the resource server endpoints like any other external client using HTTP requests containing the appropriate OAuth authentication headers.

We need a WebClient for the frontend application to interact with the resource server. WebClient is the new non-blocking solution for making HTTP calls and is the preferred option over the older RestTemplate.

To call the resource server endpoints, each request needs to have an associated access token. The WebClientConfig class code configures an instance of WebClient to include a token sourced from an OAuth2AuthorizedClient:

Java
package com.matthewcasperson.incidentmanagementdemo.providers;
 
...
// imports
...

@Configuration
public class WebClientConfiguration {
  @Bean
  public static WebClient webClient(final OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) {
    final ServletOAuth2AuthorizedClientExchangeFilterFunction function =
        new ServletOAuth2AuthorizedClientExchangeFilterFunction(oAuth2AuthorizedClientManager);
    return WebClient.builder()
        .apply(function.oauth2Configuration())
        .build();
  }
}

Create the Web Application Controller

The web application endpoints are exposed by the IncidentWebController class, detailed in the following package:

Java
package com.matthewcasperson.incidentmanagementdemo.controller;

The front end interacts with the resource server endpoints using the WebClient we created in the WebClientConfiguration class. We inject an instance of WebClient into our controller:

Java
@Controller
public class IncidentWebController {

  @Autowired
  WebClient webClient;

The root directory is exposed by the getCreateChannel method. This method injects a client called api that’s configured to access the resource server:

Java
@GetMapping("/")
public ModelAndView getCreateChannel(
    @RegisteredOAuth2AuthorizedClient("api") final OAuth2AuthorizedClient client) {

  final ModelAndView mav = new ModelAndView("create");

The page requires a list of teams and users, which are retrieved from the resource server endpoints.

Java
final List teams = webClient
    .get()
    .uri("http://localhost:8080/api/teams/")
    .attributes(oauth2AuthorizedClient(client))
    .retrieve()
    .bodyToMono(List.class)
    .block();

final List users = webClient
    .get()
    .uri("http://localhost:8080/api/users/")
    .attributes(oauth2AuthorizedClient(client))
    .retrieve()
    .bodyToMono(List.class)
    .block();

The users and teams are added as model attributes, and the ModelAndView object is returned:

Java
    mav.addObject("teams", teams);
    mav.addObject("users", users);

  return mav;
}

The Thymeleaf template renders the teams and users in lists and provides a text box where we can define the name of the new incident management channel.

Image 2

The form above submits a POST request to the channel endpoint. This is handled by the postCreateChannel method.

Java
@PostMapping("/channel")
public ModelAndView postCreateChannel(
    @RegisteredOAuth2AuthorizedClient("api") final OAuth2AuthorizedClient client,
    @RequestParam final String channelName,
    @RequestParam final String team,
    @RequestParam final List<String> users) {

The browser is redirected to the message page when this method returns:

Java
final ModelAndView mav = new ModelAndView("redirect:/message");

The new channel is created and assigned to the selected users by calling the resource server.

Java
final Channel newChannel = webClient
    .post()
    .uri("http://localhost:8080/api/teams/" + team + "/channel")
    .bodyValue(new IncidentRestController.NewChannelBody(channelName, users))
    .attributes(oauth2AuthorizedClient(client))
    .retrieve()
    .bodyToMono(Channel.class)
    .block();

The next page requires the name and ID of the new channel, as well as the team ID. These are defined as model attributes. The ModelAndView is then returned:

Java
  mav.addObject("channelId", newChannel.id);
  mav.addObject("channelName", newChannel.displayName);
  mav.addObject("team", team);
  return mav;
}

When redirecting the browser to a new page, Spring adds the model attributes as query parameters. The getCreateMessage method handles the GET request to the message page, extracts the query parameters, and redefines them in a new ModelAndView so they can be consumed by the Thymeleaf template.

Java
@GetMapping("/message")
public ModelAndView getCreateMessage(
    @QueryParam("team") final String team,
    @QueryParam("channel") final String channelId,
    @QueryParam("channel") final String channelName) {
  final ModelAndView mav = new ModelAndView("message");
  mav.addObject("team", team);
  mav.addObject("channelId", channelId);
  mav.addObject("channelName", channelName);
  return mav;
}

The message page displays a form that allows users to enter and submit a custom message to the channel, or use a predefined status message. These status messages would be configured to the business’ needs and represent the standard lifecycle of an incident.

Image 3

By posting these standard messages to the incident management channel, the engineering and support teams have a convenient communication platform. Updates only require a single button press, and all parties have a shared understanding of the state of the incident.

The form performs a POST request to the message endpoint, which is handled by the postCreateMessage method.

Java
@PostMapping("/message")
  public ModelAndView postCreateMessage(
      @RegisteredOAuth2AuthorizedClient("api") final OAuth2AuthorizedClient client,
      @RequestParam final String channelName,
      @RequestParam final String channelId,
      @RequestParam final String team,
      @RequestParam final String customMessage,
      @RequestParam final String status) {

    final ModelAndView mav = new ModelAndView("message");

The resource server is responsible for creating the new messages in Teams:

Java
webClient
    .post()
    .uri("http://localhost:8080/api/teams/" + team + "/channel/" + channelId + "/message")
    .bodyValue(status + "\nMessage: " + customMessage)
    .attributes(oauth2AuthorizedClient(client))
    .retrieve()
    .toBodilessEntity()
    .block();

The same form is then redisplayed to allow the next status message to be posted:

Java
    mav.addObject("channelName", channelName);
    mav.addObject("channelId", channelId);
    mav.addObject("team", team);
    return mav;
  }
}

Last, the custom message and status are posted to the new channel.

Image 4

Create the Resource Server Controller

The resource server is exposed by the IncidentRestController class, found in the following package:

Java
package com.matthewcasperson.incidentmanagementdemo.controller;

These are REST API endpoints, so our controller is annotated with @RestController.

Java
@RestController
public class IncidentRestController {

The information required to create a new channel and add users is defined by the NewChannelBody class:

Java
public static class NewChannelBody {

  public final String channelName;
  public final List<String> members;

  public NewChannelBody(
      final String channelName,
      final List<String> members) {
    this.channelName = channelName;
    this.members = members;
  }
}

Access to the Graph API is performed through the client created by the GraphClientConfiguration class. An instance of this client is injected here:

Java
@Autowired
GraphServiceClient<Request> client;

The getTeams method returns a list of teams.

A common pattern when interacting with the Graph API client is to use the Optional class to deal with possible null values being returned. Our app assumes a null value represents an empty list, but the production code would have more robust error handling for these situations.

Java
@GetMapping("/api/teams")
public List<Team> getTeams() {
  return Optional.ofNullable(client
          .me()
          .joinedTeams()
          .buildRequest()
          .get())
      .map(BaseCollectionPage::getCurrentPage)
      .orElse(List.of());
}

The getUsers method returns a list of users.

Java
@GetMapping("/api/users")
public List<User> getUsers() {
  return Optional.ofNullable(client
          .users()
          .buildRequest()
          .get())
      .map(BaseCollectionPage::getCurrentPage)
      .orElse(List.of());
}

We create a new channel in the createChannel method.

Java
@PostMapping("/api/teams/{team}/channel")
public Channel createChannel(
    @PathVariable("team") final String team,
    @RequestBody final NewChannelBody newChannelBody) {
  final Channel channel = new Channel();
  channel.displayName = newChannelBody.channelName;
  channel.membershipType = ChannelMembershipType.PRIVATE;

Creating a new channel involves several operations. We start by querying the Graph API for any existing channel with the same name:

Java
final List<Channel> existingChannel = Optional.ofNullable(client
        .teams(team)
        .channels()
        .buildRequest()
        .filter("displayName eq '" + newChannelBody.channelName + "'")
        .get())
    .map(BaseCollectionPage::getCurrentPage)
    .orElse(List.of());

We then either reuse the existing channel or create a new one.

Java
final Channel newChannel = existingChannel.isEmpty()
    ? client
      .teams(team)
      .channels()
      .buildRequest()
      .post(channel)
    : existingChannel.get(0);

Each of the selected users is then added to the team and then to the channel:

Java
for (final String memberId : newChannelBody.members) {
  final ConversationMember member = new ConversationMember();
  member.oDataType = "#microsoft.graph.aadUserConversationMember";
  member.additionalDataManager().put(
      "user@odata.bind",
      new JsonPrimitive("https://graph.microsoft.com/v1.0/users('" +
          URLEncoder.encode(memberId, StandardCharsets.UTF_8) + "')"));

  try {
    // add the user to the team
    client
        .teams(team)
        .members()
        .buildRequest()
        .post(member);

    // add the user to the channel
    client
        .teams(team)
        .channels(newChannel.id)
        .members()
        .buildRequest()
        .post(member);
  } catch (final Exception ex) {
    System.out.println(ex);
    ex.printStackTrace();
  }
}

The channel is then returned.

Java
  return newChannel;
}

Creating a new message is handled by the createMessage method:

Java
@PostMapping("/api/teams/{team}/channel/{channel}/message")
public void createMessage(
    @PathVariable("team") final String team,
    @PathVariable("channel") final String channel,
    @RequestBody final String message) {

We assume the incoming message is plain text, as our forms don’t expose any rich text editing. Since messages are posted using HTML, line breaks are replaced with HTML break elements:

Java
final ChatMessage chatMessage = new ChatMessage();
chatMessage.body = new ItemBody();
chatMessage.body.content = message.replaceAll("\n", "<br/>");
chatMessage.body.contentType = BodyType.HTML;

The message is then posted to the channel.

Java
    client
        .teams(team)
        .channels(channel)
        .messages()
        .buildRequest()
        .post(chatMessage);
  }
}

Conclusion

While chat platforms like Teams are convenient for general collaboration, freeform conversations aren’t a great method for conveying the status of an ongoing incident. Anyone trying to quickly grasp the current state of incident resolution work is forced to read and interpret long conversations and nested threads. By creating a limited chat interface with several predetermined status messages, teams can keep stakeholders informed with a high signal-to-noise incident management channel.

In this article, we created an example Spring Boot application that used the MSAL library and Graph API client to quickly create a new channel in Teams, add the required users, and then send predetermined status messages.

Throughout this three-part series, we’ve explored how we can use the Microsoft Identity platform and the Microsoft Authentication Library (MSAL) to build apps in the Microsoft Graph in Spring Cloud Applications. The Microsoft Graph platform and MSAL are easy for us Java developers to use, making them a strong choice for all our authentication and authorization needs.

Let Microsoft handle the painful parts of authentication and authorization so that you can focus on the parts of your applications that add the most business value. Now that we’ve explored how to use the Microsoft Identity library, MSAL, and the Microsoft Graph, start using them in your own applications.

This article is part of the series 'Building Rich Microsoft Graph Apps with MSAL and Graph SDK for Java View All

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Writer
Australia Australia

Comments and Discussions

 
-- There are no messages in this forum --
Building Rich Microsoft Graph Apps with MSAL and Graph SDK for Java