Using WebChat with Azure Bot Service’s Authentication

[Updated on 9/24/2018]

This blog covers how to use WebChat with the Azure Bot Service’s built-in authentication capability to authenticate chat users with various identity providers such AAD, GitHub, Facebook, etc, including best practices on how to ensure a secure experience. This tutorial also covers where the built-in authentication features are currently supported and where they are not.

Step 1: Registering Connections and Making a Bot

The first step is to register your connections and to create a bot that supports sending OAuthCards and receiving tokens. These steps are documented here. Once you have a bot registered, a connection set, and are able to run your bot successfully using the Bot Framework Emulator to sign a user in you are ready to build a WebChat based client.

Step 2: Enabling DirectLine

The next thing you’ll want to do is to enable DirectLine for your bot. Currently, all bots come “pre-registered” with the WebChat channel, but the authentication features currently only work if you have DirectLine enabled and are using a DirectLine secret (we are working to support WebChat secrets, but for now, use DirectLine). To register for the DirectLine channel:

  1. Go to https://portal.azure.com/
  2. Navigate to your bot resource:

3.      Click on “Channels”, then click on the DirectLine icon (it looks like a globe):

4.       Copy one of the Secret Key values and store this for later. This is your ‘DirectLine Secret’ string.

5. Enable “Enhanced authentication options”. Add at least 1 trusted origin. These are where you want to host Web Chat. For example, if you host Web Chat at https://contoso.com then enter “https://contoso.com” as a trusted origin. If you test Web Chat on localhost, then add http://localhost as a trusted origin. Learn more about these options here.

Your bot is now enabled to use DirectLine and configured for authentication and we can proceed to the next steps.

Step 3: Creating a Client Controller

While there are many mechanisms to invoke WebChat on a client HTML page, one thing that is very important to do is to ensure you keep your DirectLine Secret a secret! The DirectLine Secret is as important as a database connection string and these sorts of things should never show up in HTML or even in the JavaScript that is running behind an HTML page.

Instead what you need to do is to keep your DirectLine Secret hidden in a client controller. This client controller should exchange the DirectLine Secret for a DirectLine Token, which CAN be exposed in HTML and JavaScript. You can find lots of details about this here, but the summary is that a DirectLine Secret can connect to ANY conversation whereas a DirectLine Token is bound to a single conversation.

A good practice for securing the DirectLine Secret is to create a client controller that performs the exchange for a DirectLine Token and then passes this DirectLine Token back to the HTML page.

Here is an example ASP.NET controller that does this and returns a DirectLine Token:

public class HomeController : Controller
{
    public async Task<ActionResult> Index()
    {
        var secret = GetSecret();

        HttpClient client = new HttpClient();

        HttpRequestMessage request = new HttpRequestMessage(
            HttpMethod.Post,
            $" https://directline.botframework.com/v3/directline/tokens/generate");

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secret);

        
        var response = await client.SendAsync(request);
        string token = String.Empty;

        if (response.IsSuccessStatusCode)
        {
            var body = await response.Content.ReadAsStringAsync();
            token = JsonConvert.DeserializeObject<DirectLineToken>(body).token;
        }

        var config = new ChatConfig()
        {
            Token = token,
        };

        return View(config);
    }
}

public class DirectLineToken
{
    public string conversationId { get; set; }
    public string token { get; set; }
    public int expires_in { get; set; }
}
public class ChatConfig
{
    public string Token { get; set; }
}

And here is a node.js controller that does the same:

var router = express.Router();          // get an instance of the express Router

// Get a directline configuration (accessed at GET /api/config)
router.get('/config', function(req, res) {
    const options = {
        method: 'POST',
        uri: 'https://directline.botframework.com/v3/directline/tokens/generate',
        headers: {
            'Authorization': 'Bearer ' + secret
        }
    };

    request.post(options, (error, response, body) => {
        if (!error && response.statusCode < 300) {
            res.json({ 
                    token: body.token,
                });
        }
        else {
            res.status(500).send('Call to retrieve token from DirectLine failed');
        } 
    });
});

Step 3: Trusted Origins and Tamper-proof user IDs

Trusted origins

When an OAuthCard shows up in a client as a sign-in link or button, the sign-in link needs to be protected from attackers who try to steal other people’s tokens. The gist of the attack is that an attacker starts a conversation with a bot to get a sign-in link. The attacker sends the sign-in link to someone else and tells him or her to click on the link. When that person does, he or she will be asked to sign-in and the result of that flow will present the bot with a token to use for that conversation; but that conversation is the attacker’s conversation. To prevent this, we want to guarantee that the sign in link can only be used in the same browser session as the OAuthCard.

One approach is to present a final factor code at the end of sign-in that the user must type into the chat window before the bot can retrieve the user’s token. This is often referred to as the “magic code”. This however doesn’t guarantee that the sign-in link can only be used in the same browser session. The attacker can still send the sign-in link and use phishing techniques to try to get another user’s magic code.

With WebChat, we added a mechanism that’s both more secure and users do not need to deal with any “magic code”. This mechanism ensures that a user can only sign-in through the sign-in link in the same browser session as the OAuthCard. There are two parts of this mechanism:

  1. Using a list of ‘trusted origin’ hostnames that can request a WebChat session id
  2. Associate a unique signed session id cookie with the OAuthCard.

The browser that is viewing WebChat must have 3rd party cookies enabled for this to work

Tamper-proof user IDs

The authentication capabilities in Azure Bot Service acquire user tokens for a given user using a connection on a particular bot. The way Azure Bot Service distinguishes which user it’s acquiring a token for is using the User.Id that comes through on Activities. In the case of WebChat, this User.Id is modifiable by the client. If an attacker knows another user’s user ID, they can modify the WebChat control in their browser to pretend to be that user. There are a couple of ways to mitigate against this.

  1. Ensure the user IDs are not guessable.
  2. Detect that a client has changed the user ID and reject the change.

The first mitigation is up to you. A good way is to generate these within your controller method. If your website is anonymous, then you should generate a unique value within the controller and return this to your client. If your website already is using some sort of authentication, then using a bearer token (or its hash for shorter IDs) would suffice. Do not use email addresses or regular names as these are easily guessed.

We’ve implemented platform support for #2.

With the “Enhanced authentication options” that you enabled earlier, all you need to do to leverage both the no magic code safe sign in and the tamper-proof user ID is to specify a user ID when you exchange the Direct Line Secret for a Direct Line token. Note that the user ID must begin with “dl_”. To learn more about these options, go here.

Here is a look at the final controller method in ASP.NET that does the Direct Line Secret exchange with a  unique generated user id:

public class HomeController : Controller
{
    public async Task<ActionResult> Index()
    {
        var secret = GetSecret();

        HttpClient client = new HttpClient();

        HttpRequestMessage request = new HttpRequestMessage(
            HttpMethod.Post,
            $" https://directline.botframework.com/v3/directline/tokens/generate");

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secret);

            var userId = $"dl_{Guid.NewGuid()}";
            request.Content = new StringContent(
                JsonConvert.SerializeObject(
                    new { User = new { Id = userId } }),
                    Encoding.UTF8,
                    "application/json");

        var response = await client.SendAsync(request);
        string token = String.Empty;

        if (response.IsSuccessStatusCode)
        {
            var body = await response.Content.ReadAsStringAsync();
            token = JsonConvert.DeserializeObject<DirectLineToken>(body).token;
        }

        var config = new ChatConfig()
        {
            Token = token,
            UserId = userId Guid. NewGuid(). ToString()
        };

        return View(config);
    }
}

public class DirectLineToken
{
    public string conversationId { get; set; }
    public string token { get; set; }
    public int expires_in { get; set; }
}
public class ChatConfig
{
    public string Token { get; set; }
    public string UserId { get; set; }
}

And here is a node.js controller that does the same:

// Get a directline configuration (accessed at GET /api/config)
const userId = "dl_" + createUniqueId();

router.get('/config', function(req, res) {
    const options = {
        method: 'POST',
        uri: 'https://directline.botframework.com/v3/directline/tokens/generate',
        headers: {
            'Authorization': 'Bearer ' + secret
        },
        json: {
            User: { Id: userId }
        }
    };

    request.post(options, (error, response, body) => {
        if (!error && response.statusCode < 300) {
            res.json({ 
                    token: body.token,
                    userId: userId
                });
        }
        else {
            res.status(500).send('Call to retrieve token from DirectLine failed');
        } 
    });
});

Step 4: Creating the WebChat HTML page

There are several ways of including WebChat within an HTML page and these are all outlined here. Not all of these are appropriate for use when building a website that performs secure logins:

  • Using an IFRAME is not a supported way of driving the authentication flow using OAuthCards because you cannot set a trusted origin and you cannot set user ids
  • Running WebChat inline from the CDN or your own copy of WebChat is supported.
  • Running WebChat as a React component is supported.

Here is an example of a “Home” view for the ASP.NET controller defined earlier:

@model ChatConfig
@{
    ViewData["Title"] = "Home Page";
}

<link href="https://cdn.botframework.com/botframework-webchat/latest/botchat.css" rel="stylesheet" />
<div id="bot" />
<script src="https://cdn.botframework.com/botframework-webchat/latest/botchat.js"></script>
<script>
      BotChat.App({
          directLine: {
              secret: '@Model.Token'
          },
        user: { id: @Model.UserId },
        bot: { id: 'botid' },
        resize: 'detect'
      }, document.getElementById("bot"));
</script>

Here is an example of a React instantiation of WebChat that uses the token and userId:

<Chat 
    directLine={{ 
      secret: this._token,
    }}

    speechOptions={speechOptions}

    bot={{ id: 'bot', name: '' }}
    user={{ id: this._userId, name: 'Peter' }}
/>

Summary

While there are a number of steps in this tutorial to create a secure WebChat client that allows user login, the basic steps involve:

  • Performing DirectLine Secret to Token exchange on a client controller
  • Passing in TrustedOrigin values when performing secret to token exchange
  • Ensuring that each user has a unique, non-guessable user id value that is used with WebChat
  • Choosing the proper way to place WebChat into your HTML page: by inlining it or using a React component but NOT using an IFRAME