Microsoft BOT framework, transparent authentication with the webchat control

Hi,

In this post, I will explain how you can transparently authenticate end users to a BOT whose the backend is hosted in Azure.

I’m only covering the webchat channel and more particularly the webchat control that is available out of the box when enabling the web chat channel in the BOT configuration page.  As this sample leverages various building blocs, I assume that you already know ADAL and the Microsoft BOT framework.

At the time of writing this blog post, the framework is still in preview so things are subject to change in the future.

A good reading regarding BOT authentication is the following article written by Tsuyoshi Matsuzaki. In that article, Tsuyoshi explains how to leverage the built-in login UI plus two other techniques. What he explains is perfectly suitable in some situations and for all the channels but always requires a manual intervention from the end user. In my particular scenario, I want to make use of the webchat control inside of an organization, so the BOT isn’t meant to be consumed worldwide. I can control the population that will access it so I can deal with the scalability aspects.

So, in a nutshell, the idea consists in creating an ASP.NET WebChat controller that is protected by Azure AD. That controller performs the HTTP request against the https://webchat.botframework.com/embed/?s=&userid=&username=” endpoint, passing in the secret, the userid and the username. The latter parameters are extracted from the ClaimsPrincipal.Current object. Both the userid and username parameters will be transmitted to your BOT and be available from the activity object that is transmitted by the BOT framework.

On top of extracting the parameters in the WebChat controller, we can directly grab an AccessToken for the Microsoft Graph for the current user and save it in the BOT state using the userid as a key. This will allow the BOT to interact with the Graph API on behalf of the current user.

The overall result is that if you use the webchat control from a SharePoint Online site or from an Azure WebApp that’s already protected by AAD, you will have a transparent authentication and a ready-to-use AccessToken for the current user. Here is a screenshot of this sample BOT:

bot3

As an end user, I simply say something and the BOT queries the Graph to return my picture and my email address. I didn’t have to use the Login UI from the BOT framework nor to send any kind of magic code. I simply started the conversation. Of course, this BOT has no business value but the purpose is to demonstrate how to authenticate, get an AccessToken and use it from the BOT.

I’m not going through all the steps here as it’d take too long. Note that the entire source code is available on GitHub. However, don’t run away as the some configuration is required to get something up and running. I will describe below what needs to be done.

So, here are the steps:

  • Create an Azure Web App, in this example I’ll work with transparent-auth-bot.azurewebsites.net and download the publishing profile. This Web App will host the project download from GitHub.
  • Create an AAD App of type Web and grant it some delegate permissions to the Graph API. In the Sign-in URL, specify the URL (host header only) from the Azure Web App created earlier. Create a Key (app password) and save it somewhere as we’ll use it later on. Note that you should make an admin consent of this app so that users won’t be prompted to consent since that isn’t feasible from an iframe (used to render the webchat control).
  • Register your BOT (https://dev.botframework.com/), follow the wizard and make sure you specify a valid messaging endpoint. In my example, the messaging endpoint is https://transparent-auth-bot.azurewebsites.net/messages/api. Grab the Bot ID & Password as we’ll need it later on. Here is an example of my BOT once created:
    bot1
  • Configure the WebChat channel to grab the secret you’ll need as a query string parameter later.
  • Download the GitHub project and make sure to register the outcome of the previous configuration steps into the web.config.

In a nutshell, what you’ll see in the GitHub project is the authentication plumbing required to protect the webchat controller.

Let’s now focus on the controllers themselves, the first one is the message controller that cannot be protected by Azure AD as the Microsoft layer will use the BOT authentication mechanism, hence why this controller must be decorated with the [BotAuthentication] attribute. The second controller is our WebChat controller that will return the content to the calling iframe.

Note that these two controllers could be part of different APIs, I’m regrouping them here to deliver a single solution with a single project for sake of simplicity but that’s clearly not a requirement.

WebChat controller

[Authorize]
    public class WebChatController : ApiController
    {
        private string botSecret = ConfigurationManager.AppSettings["BotSecret"];
        private string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
        private string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
        private string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
        private string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
        private string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
        private string UserAccessToken = null;
        public HttpResponseMessage Get()
        {
            var userId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
            GetTokenViaBootStrap().Wait();
            var botCred = new MicrosoftAppCredentials(
                ConfigurationManager.AppSettings["MicrosoftAppId"],
                ConfigurationManager.AppSettings["MicrosoftAppPassword"]);
            var stateClient = new StateClient(botCred);
            BotState botState = new BotState(stateClient);
            BotData botData = new BotData(eTag: "*");
            botData.SetProperty<string>("GraphAccessToken", UserAccessToken);
            stateClient.BotState.SetUserDataAsync("webchat", userId, botData).Wait();
            string WebChatString =
                new WebClient().DownloadString("https://webchat.botframework.com/embed/transparentauth?s="+botSecret+"&userid=" +
                HttpUtility.UrlEncode(userId) + "&username=" + HttpUtility.UrlEncode(ClaimsPrincipal.Current.Identity.Name));
            WebChatString = WebChatString.Replace("/css/botchat.css", "https://webchat.botframework.com/css/botchat.css");
            WebChatString = WebChatString.Replace("/scripts/botchat.js", "https://webchat.botframework.com/scripts/botchat.js");
            var response = new HttpResponseMessage();
            response.Content = new StringContent(WebChatString);
            response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html");
            return response;
        }
        private Task GetTokenViaBootStrap()
        {
            return Task.Run(async () =>
            {
                var bc = ClaimsPrincipal.Current.Identities.First().BootstrapContext
                    as System.IdentityModel.Tokens.BootstrapContext;

                string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
                string userAccessToken = bc.Token;
                UserAssertion userAssertion = new UserAssertion(bc.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
                string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
                string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
                AuthenticationContext authContext = new AuthenticationContext(aadInstance + tenantId, new ADALTokenCache(signedInUserID));
                ClientCredential cred = new ClientCredential(clientId,appKey);
                AuthenticationResult res = await authContext.AcquireTokenAsync("https://graph.microsoft.com", cred,
                    userAssertion);
                UserAccessToken = res.AccessToken;
            });

        }
    }

The controller is decorated with the [Authorize] attriibute which ensures that any of its method is protected (see the plumbing from the GitHub project). We start by extracting the userid from the current principal, then we get a fresh AccessToken for the Graph API using AcquireTokenSilentAsync. Once we have the token, we save it into a BotState Data object that we bind to the userid. The Bot State service uses the combination of the BOT credentials, the channel and the UserID to store the data.

The second part of this controller is to perform the request to the out of the box webchat control, passing in our secret, userid and username parameters. We get an HTTP response back. We have to rewrite the references to the .js and .css files as the webchat control makes use of relative paths. This results in having references such as https://yourazurewebapp…/css&#8230;.which is of course wrong since these files live in their own domain. Note that if you don’t want any dependency with the online chat control, you can ghost your own version by rebuilding your own webchat control, see this link for more info.

So, here we’ve saved the AccessToken into the BotState data object so that the BOT can reuse it when necessary.

Message controller

The message controller is the one that is called whenever a new conversation starts. What you must understand is that this controller is not called directly by one of your components. The communication flow is the following: the webchat controll talks to directline.botframework.com and this guy talks to your message controller using the messaging endpoint you configured while registering the BOT.

That said, here is the code:

public class UserInfo
    {
        public string DisplayName { get; set; }
        public string Email { get; set; }
        public string PhoneNumber { get; set; }
        public Stream PhotoStream { get; set; }
    }
    [BotAuthentication]
    public class MessagesController : ApiController
    {

        public async Task<UserInfo> GetUserInfo(GraphServiceClient graphClient)
        {
            User me = await graphClient.Me.Request().GetAsync();

            return new UserInfo
            {
                Email = me.Mail ?? me.UserPrincipalName,
                DisplayName = me.DisplayName,
                PhotoStream= await graphClient.Users[me.Id].Photo.Content.Request().GetAsync()
            };
        }
        GraphServiceClient GetAuthenticatedClient(string token)
        {
            GraphServiceClient graphClient = new GraphServiceClient(
                new DelegateAuthenticationProvider(
                    async (requestMessage) =>
                    {
                        string accessToken = token;
                        // Append the access token to the request.
                        requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
                    }));
            return graphClient;
        }
        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            switch (activity.GetActivityType())
            {
                case ActivityTypes.Message:
                    var reply = activity.CreateReply();
                    reply.Attachments = new List<Microsoft.Bot.Connector.Attachment>();
                    var cli = new ConnectorClient(new Uri(activity.ServiceUrl));
                    try
                    {

                        StateClient stateClient = activity.GetStateClient();
                        BotState botState = new BotState(stateClient);
                        BotData botData = await botState.GetUserDataAsync(activity.ChannelId, activity.From.Id);
                        string token = botData.GetProperty<string>("GraphAccessToken");

                        if (!string.IsNullOrEmpty(token))
                        {
                            GraphServiceClient gc = GetAuthenticatedClient(token);
                            UserInfo ui = await GetUserInfo(gc);
                            var memoryStream = new MemoryStream();
                            ui.PhotoStream.CopyTo(memoryStream);
                            reply.Attachments.Add(new Microsoft.Bot.Connector.Attachment
                            {
                                Content = memoryStream.ToArray(),
                                ContentType = "image/png"
                            });
                            reply.Attachments.Add(new HeroCard()
                            {
                                Title = ui.DisplayName,
                                Text = ui.Email,
                                Images = null,
                                Buttons = null
                            }.ToAttachment());

                            //reply.AttachmentLayout = AttachmentLayoutTypes.Carousel;
                        }
                        else
                        {
                            reply.Text = "No token!";
                        }
                    }
                    catch(Exception ex)
                    {
                        reply.Text = ex.Message;
                    }
                    await cli.Conversations.SendToConversationAsync(reply);
                    break;
                case ActivityTypes.ConversationUpdate:
                    var client = new ConnectorClient(new Uri(activity.ServiceUrl));
                    IConversationUpdateActivity update = activity;

                    if (update.MembersAdded.Any())
                    {

                        var newMembers = update.MembersAdded?.Where(t => t.Id != activity.Recipient.Id);
                        foreach (var newMember in newMembers)
                        {
                            var r = activity.CreateReply();
                            r.Text = "Welcome";
                            await client.Conversations.ReplyToActivityAsync(r);
                        }
                    }
                    break;
                case ActivityTypes.ContactRelationUpdate:
                case ActivityTypes.Typing:
                case ActivityTypes.DeleteUserData:
                case ActivityTypes.Ping:
                default:
                    HandleSystemMessage(activity);
                    break;
            }
            var response = Request.CreateResponse(HttpStatusCode.OK);
            return response;

        }
        private Activity HandleSystemMessage(Activity message)
        {
            if (message.Type == ActivityTypes.DeleteUserData)
            {
                // Implement user deletion here
                // If we handle user deletion, return a real message
            }
            else if (message.Type == ActivityTypes.ConversationUpdate)
            {
                // Handle conversation state changes, like members being added and removed
                // Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info
                // Not available in all channels
            }
            else if (message.Type == ActivityTypes.ContactRelationUpdate)
            {
                // Handle add/remove from contact lists
                // Activity.From + Activity.Action represent what happened
            }
            else if (message.Type == ActivityTypes.Typing)
            {
                // Handle knowing tha the user is typing
            }
            else if (message.Type == ActivityTypes.Ping)
            {
            }

            return null;
        }

    }

When the BOT receives a message, it extracts the AccessToken from the BotState data object and performs a query against the Graph API using the SDK. The BOT is able to get this data thanks to both activity.Channel.Id and activity.From.Id that respectively contain “webchat” (in case of the webchat control of course) and the userid we passed in in our webchat controller. Should you use the exact same code through another channel, the BOT wouldn’t find any data in the BotState.

Bottom line

Why using a custom controller instead of simply passing the userid directly to the webchat control in the iframe? I see two good reasons: the 1st one is that thanks to our webchat controller, we can easily generate an AccessToken to anything (my example is for the Graph but it could have been Yammer as well) and the 2nd reason is because if you pass the userid directly, this will be visible in the source code of the page and exposed to vulnerabilities (users could start fiddling with that parameter). Using a proxy, you simply specify the path to your controller without any parameter. Your controller (server-side) will then safely perform the web request (server to server) passing in the secret and the userid that cannot be altered by end users. Of course, the answer will return botchat.js that will reveal the secet & userid if you launch the developer tools. The Azure AD user identifier is not so easily predictable but if you really want to be secure, you might consider passing a combination of UserId/GUID in the userid parameter which should definitely be more secure.

There is one drawback to this approach though: the scalability. Since the iframes will point to your controller and no more directly to Microsoft’s, you’ll have to make sure to run multiple instances of Azure WebApps to support the workload. On the other hand, you needed to think of it to host your message controller that is a must-have unless you go for the BaaS approach which uses Azure Functions.

Note: in this example, the interaction with AAD to get an AccessToken isn’t robust enough because if the conversation lasts more than an hour, the saved AccessToken will expire, thus, you should plan for a fallback mechanism and handle that problem.

Feel free to watch my videos on Azure Cognitive Services

Happy Coding!

About Stephane Eyskens

Office 365, Azure PaaS and SharePoint platform expert
This entry was posted in Azure, Azure Active Directory and tagged . Bookmark the permalink.

26 Responses to Microsoft BOT framework, transparent authentication with the webchat control

  1. Pingback: Transparent BOT authentication with Microsoft Teams | Welcome to my blog! Stéphane Eyskens, Office 365 and Azure PaaS Architect

  2. Pingback: Building the nextgen SharePoint search through a BOT and LUIS? | Welcome to my blog! Stéphane Eyskens, Office 365 and Azure PaaS Architect

  3. ashay says:

    Hi,
    must say great post..
    I have implemented same, however, generating own token.
    our tokens are lengthy. I can say they are of 620 characters length. total url size becomes 650characters including username , bot secret.
    but when I hit enter, it shows couldnt send. however, web chat is on.
    If I reduce the size of token to 64 charcters, it works.
    activity.id doesnt accept my token if it is more than that length.
    can you suggest something on this.

    Thanks,
    Ashay

    Like

    • Stephane Eyskens says:

      Hi,

      That’s not what I’m doing. I’m only protectecting my controller endpoint with Azure AD. Then I’m passing in the userid & username but not any kind of token. However, when I’ve authenticated the user, I’m generating an access token which I store in the bot state. So, that’s probably what you should do as well instead of passing the token in the URL.

      Like

  4. Pingback: Contextual authentication with the Bot webchat control in SharePoint

  5. sai chundur says:

    hi I tried your sample I get after welcome a message and then No Token . It is not giving my profile info
    Can you please advise
    thanks

    Like

  6. Clément says:

    Hello Stephane,

    Very good article !
    It helped me to clarify some important points :).
    One thing i would like to ask, you show how to use a transparent auth using a “subsite” tab.
    You mention that the direct one to one chat is not really feasible when you write your article, does it changed since then ?

    Thanks a lot,

    Like

    • Stephane Eyskens says:

      Hi Clément,

      Not sure what you exactly means by a subsite but the reason why I’m using an Azure webapp is because when users call my webchat endpoint, I don’t only authenticate them but I also grab an access token directly which I save in the bot state for that particular user. That’s not something that’s feasible if you call directly the webchat control that’s made available by Microsoft. You need the extra hop to generate and record the access token. If this is not required in your scenario, then you can simply transmit the userid & name via the webchat parameters.

      Like

  7. Pingback: Seamless SSO in Chat Bot | ASK Dev Archives

  8. Pingback: Microsoft Bot transparent authentication * ChatBots

  9. Hello Stephane,
    This is really a good example, just wanted to have couple of clarifications(it may help others who might want to run your code):
    1. The webchat in the your demo is hosted in default.htm(/api/webchat)
    Or will it be ok to host the iframe tag in any authenticated webapp page with absolute path ofcourse.
    2. You have two DBs there “aspnet-transparent-auth-bot-20170104020212.mdf” and “AuthenticationDB”, just wanted to have a bit more info on this, where are you hosting these, are you trying to create these in runtime inmemory or do we have sql DB for these. A bit of guidance on that in the blog will be good 🙂

    Thanks!
    Dip

    Like

    • Stephane Eyskens says:

      Hi Dip, you can embed it from anywhere (because I don’t prevent it from the webchat endpoint). The AuthenticationDB is simply hosted in Azure SQL Server 🙂

      Like

  10. Piotr says:

    Hi Stephane

    Have you got any problems while switching between users on sharepoint ? I mean that you’ve logged in with User A and webchat recognized you as UserA and you logged out and logged in with user B and webchat imeddiately recognized you as UserB ?

    Piotr

    Like

    • Stephane Eyskens says:

      Hi Piotr,

      That’s a typical issue that’s not related to the bot framework but to browsers themselves. Whenever you authenticate, a session or persistent cookie will be stored in the browser to tell SharePoint (and any other relying party) that you’re authenticated. The switch between users is often confusing because of that cookie stored in the browser.

      So, yes, you might see odd behaviors but, except trying your stuff in private/incognito mode, I don’t have any other recommendation.

      Like

  11. Hi Stephane

    I tried to implement this silent authentication in my bot and when I am implementing this, I am getting 502 error bad gateway

    . I was authenticated successfully
    I got bot chat window opened
    When I say “hi”, in the network trace I am getting error 502 bad gate way.

    -Premchand

    Like

  12. Gaurav says:

    Hi Stephen,
    I’m facing issues while running this application. Getting following error:
    After running the application from visual studio or directly using the URL of published App, I get the error as
    Server Error in ‘/’ Application.
    The resource cannot be found.
    Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.
    Requested URL: /
    ——————————————
    The steps I have done to run the application are:
    1: Created a Web App on Azure portal on my free subscription with admin account
    2: Created an Azure AD App of type Web and gave the sign in url as the URL of my web app from step 1, created a key for this app. Capture Application ID. Selected read directory, read all basic profiles, read all groups delegated permissions.
    3: Register a Bot, capture Microsoft App ID, password and Web Chat channel Secret. Updated endpoint as https://mybotname.azurewebsites.net/messages/api
    mybotname value is different.
    Selected similar Microsoft Graph Permissions in application registration portal
    4: Downloaded your Github project, updated following parameter in web.config
    Botid: Bot handle given during Bot registration
    BotSecret: Web channel secret
    MicrosoftAppId and MicrosoftAppPassword values captured during 3rd step
    ClientId: Application ID captured during Azure AD application registration
    ClientSecret: value obtained during step 2
    Domain: value found in domain names in Azure AD
    TenantId: Directory ID Value from Azure AD under properties
    Replaced RedirectUri and PostLogoutRedirectUri app name with my web app name
    5: Published the application from the visual studio in web app created on 1st step.
    6: Run the application

    Can you please tell me if I’m missing any step? I have azure trail subscription with my personal account.

    Like

  13. Stephane Eyskens says:

    Hello,

    It might be a typo of yours but we never know, you mentioned:

    https://mybotname.azurewebsites.net/messages/api

    but the url should be

    https://mybotname.azurewebsites.net/api/messages

    Also make sure you registered the right URL endpoint during the bot registration

    Best Regards

    Like

  14. François says:

    Hello Stéphane,

    I’m hosting my first chatbot inside a asp.net web app in azure.
    Before creating a webchat controler, I wanted to test my chat inside my asp.net page (embed iframe)

    I’ve added userid and username as querystring on the src of the iframe.
    https://webchat.botframework.com/embed/mybot?s=SECRET&userid=ABC123&username=UserName

    It’s working fine…except I can’t get the userid and username I passed…Where are they located?
    I tried activity.From.Name and activity.From.Id but It’s not there…
    I guess It’s somewhere on the activity object…but I can’t find It.

    In fact I haven’t found a way to debug the webchat when embed in an iframe…Is there a way?

    Keep-up the good work,

    François

    Like

  15. Rohit says:

    Hi Stephane,
    BotState is deprecated now. How should we maintain a user state mainly, What is the alternative to send user auth token to BOT so that we can get the httpContext for that user.

    Like

    • Stephane Eyskens says:

      Hello,

      Well, you can follow the official recommendations: https://docs.microsoft.com/en-us/bot-framework/dotnet/bot-builder-dotnet-state

      That’s the problem with preview features, it’s always subject to changes…

      Best Regards

      Like

      • Rohit says:

        Stephane, thanks for your valuable reply.

        I already went through the url that you have shared, & I feel things are not clear there.

        I could create Azure table storage and bot framework could save the data in it. But did not find any mechanism similar to what you have given in WebChatController – :

        var botCred = new MicrosoftAppCredentials(
        ConfigurationManager.AppSettings[“MicrosoftAppId”],
        ConfigurationManager.AppSettings[“MicrosoftAppPassword”]);
        var stateClient = new StateClient(botCred);
        BotState botState = new BotState(stateClient);
        BotData botData = new BotData(eTag: “*”);
        botData.SetProperty(“GraphAccessToken”, UserAccessToken);
        stateClient.BotState.SetUserDataAsync(“webchat”, userId, botData).Wait();

        As StateClient is deprecated how should we set the Auth token in the bot object so that it will be made available in BOT (MessageController.cs)

        Your reply is eagerly awaited!

        Regards,
        Rohit

        Like

      • Stephane Eyskens says:

        Hello Rohit,

        I have not digged that further but you might want to defer the login and prompt the user from within the dialog as described here https://blogs.msdn.microsoft.com/richard_dizeregas_blog/2017/05/15/bot-authentication-in-the-bot-framework/

        Like

  16. Sanket Thotange says:

    I am getting this error in iframe. “BotAuthenticator failed to authenticate incoming request!”

    Like

Leave a comment