Botframework-sdk: Conversation.SendAsync Authentication using custom multi-tenant authentication (Bot Builder 3.5.2)

Created on 14 Feb 2017  路  7Comments  路  Source: microsoft/botframework-sdk

I am in the process of creating several different bots that will tie into Facebook Messager. Each of these bots will have the potential to have 10-100 different Facebook pages connected to them. This project was originally a POC, created on 3.0.0, that escalated into something much more very rapidly.

So far I have upgraded to 3.5.2, and fully implemented my own BotCredentialProvider from the ICredentialProvider interface.
This seems to work properly, granting me access to the my controller if the MicrosoftAppId exists and is authorized on our system.

From this point we spool up a new Dialog and begin setting up the connector. In a similar fashion to how BotCredentialProvider work, we instantiate a new MicrosoftAppCredentials class passing the MicrosoftAppId and MicrosoftAppPassword (NOT FROM THE WEB CONFIG).

These credentials are used in a new ConnectorClient like so

MicrosoftAppCredentials cred = new MicrosoftAppCredentials(appId: microsoftAppId, password: microsoftAppPassword);
ConnectorClient connector = new ConnectorClient(new Uri(activity.ServiceUrl), cred);

We then get our StateClient from the activity and retrieve the BotData from the conversation.

StateClient stateClient = activity.GetStateClient(cred, activity.ServiceUrl);
BotData botData = stateClient.BotState.GetConversationData(activity.ChannelId, activity.Conversation.Id);

This all seems to work right and we are able to retrieve the information using the latest emulator.

The issue occurs on this line of code.
await Conversation.SendAsync(activity, CreateEnrollmentForm);
This line of code seems to interact for a brief second with the emulator before returning an Unauthorized response.

Note that before this I am able to successfully send a "typing" message to the bot using

Activity reply = activity.CreateReply();
reply.Type = ActivityTypes.Typing;
reply.Text = null;
await connector.Conversations.ReplyToActivityAsync(reply);

Am I doing something incorrectly? There are no obvious ways of supplying credentials to the static class Conversation that I am aware of. Is there another way to start off this process without using the static class?

Restoring the MicrosoftAppId and MicrosoftAppPassword in the web config resolves the issue, however that is not a viable solution for us as this data will need to come from a database.

Also, is there any reason why the payload of the JWT token differs between using the emulator and running on a server. Most notably the presence of "appid" when using the emulator and having the appid be in the audience when called from a server. One would think that this behavior would be the same.

Thank you,
James

Most helpful comment

similar to #2214. Here is a sample showing how you can configure the Conversation.Container with the right MicrosoftAppCredentials and use a simple MultiCredentialProvider to authenticated multiple MicrosoftAppId and MicrosoftAppPassword.

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Description;
using Autofac;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Internals;
using Microsoft.Bot.Connector;

namespace Microsoft.Bot.Sample.SimpleMultiCredentialBot
{

    /// <summary>
    /// A sample ICredentialProvider that is configured by multiple MicrosoftAppIds and MicrosoftAppPasswords
    /// </summary>
    public class MultiCredentialProvider : ICredentialProvider
    {
        public Dictionary<string, string> Credentials = new Dictionary<string, string>
        {
            { "YOUR_MSAPP_ID_1", "YOUR_MSAPP_PASSWORD_1" },
            { "YOUR_MSAPP_ID_2", "YOUR_MSAPP_PASSWORD_2" }
        };

        public Task<bool> IsValidAppIdAsync(string appId)
        {
            return Task.FromResult(this.Credentials.ContainsKey(appId));
        }

        public Task<string> GetAppPasswordAsync(string appId)
        {
            return Task.FromResult(this.Credentials.ContainsKey(appId) ? this.Credentials[appId] : null);
        }

        public Task<bool> IsAuthenticationDisabledAsync()
        {
            return Task.FromResult(!this.Credentials.Any());
        }
    }

    /// Use the MultiCredentialProvider as credential provider for BotAuthentication
    [BotAuthentication(CredentialProviderType = typeof(MultiCredentialProvider))]
    public class MessagesController : ApiController
    {


        static MessagesController()
        {

            // Update the container to use the right MicorosftAppCredentials based on
            // Identity set by BotAuthentication
            var builder = new ContainerBuilder();

            builder.Register(c => ((ClaimsIdentity)HttpContext.Current.User.Identity).GetCredentialsFromClaims())
                .AsSelf()
                .InstancePerLifetimeScope();
            builder.Update(Conversation.Container);
        }

        /// <summary>
        /// POST: api/Messages
        /// receive a message from a user and send replies
        /// </summary>
        /// <param name="activity"></param>
        [ResponseType(typeof(void))]
        public virtual async Task<HttpResponseMessage> Post([FromBody] Activity activity)
        {
            if (activity != null)
            {
                switch (activity.GetActivityType())
                {
                    case ActivityTypes.Message:
                        await Conversation.SendAsync(activity, () => new EchoDialog());
                        break;

                    case ActivityTypes.ConversationUpdate:
                        IConversationUpdateActivity update = activity;
                        // resolve the connector client from the container to make sure that it is 
                        // instantiated with the right MicrosoftAppCredentials
                        using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))
                        {
                            var client = scope.Resolve<IConnectorClient>();
                            if (update.MembersAdded.Any())
                            {
                                var reply = activity.CreateReply();
                                foreach (var newMember in update.MembersAdded)
                                {
                                    if (newMember.Id != activity.Recipient.Id)
                                    {
                                        reply.Text = $"Welcome {newMember.Name}!";
                                        await client.Conversations.ReplyToActivityAsync(reply);
                                    }
                                }
                            }
                        }
                        break;
                    case ActivityTypes.ContactRelationUpdate:
                    case ActivityTypes.Typing:
                    case ActivityTypes.DeleteUserData:
                    case ActivityTypes.Ping:
                    default:
                        Trace.TraceError($"Unknown activity type ignored: {activity.GetActivityType()}");
                        break;
                }
            }
            return new HttpResponseMessage(System.Net.HttpStatusCode.Accepted);
        }
    }
}

This is the key part showing, how you can update the Conversation.Container with the right MicrosoftAppCredentials:

var builder = new ContainerBuilder();
builder.Register(c => ((ClaimsIdentity)HttpContext.Current.User.Identity).GetCredentialsFromClaims())
           .AsSelf()
           .InstancePerLifetimeScope();
builder.Update(Conversation.Container);

All 7 comments

similar to #2214. Here is a sample showing how you can configure the Conversation.Container with the right MicrosoftAppCredentials and use a simple MultiCredentialProvider to authenticated multiple MicrosoftAppId and MicrosoftAppPassword.

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.Description;
using Autofac;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Internals;
using Microsoft.Bot.Connector;

namespace Microsoft.Bot.Sample.SimpleMultiCredentialBot
{

    /// <summary>
    /// A sample ICredentialProvider that is configured by multiple MicrosoftAppIds and MicrosoftAppPasswords
    /// </summary>
    public class MultiCredentialProvider : ICredentialProvider
    {
        public Dictionary<string, string> Credentials = new Dictionary<string, string>
        {
            { "YOUR_MSAPP_ID_1", "YOUR_MSAPP_PASSWORD_1" },
            { "YOUR_MSAPP_ID_2", "YOUR_MSAPP_PASSWORD_2" }
        };

        public Task<bool> IsValidAppIdAsync(string appId)
        {
            return Task.FromResult(this.Credentials.ContainsKey(appId));
        }

        public Task<string> GetAppPasswordAsync(string appId)
        {
            return Task.FromResult(this.Credentials.ContainsKey(appId) ? this.Credentials[appId] : null);
        }

        public Task<bool> IsAuthenticationDisabledAsync()
        {
            return Task.FromResult(!this.Credentials.Any());
        }
    }

    /// Use the MultiCredentialProvider as credential provider for BotAuthentication
    [BotAuthentication(CredentialProviderType = typeof(MultiCredentialProvider))]
    public class MessagesController : ApiController
    {


        static MessagesController()
        {

            // Update the container to use the right MicorosftAppCredentials based on
            // Identity set by BotAuthentication
            var builder = new ContainerBuilder();

            builder.Register(c => ((ClaimsIdentity)HttpContext.Current.User.Identity).GetCredentialsFromClaims())
                .AsSelf()
                .InstancePerLifetimeScope();
            builder.Update(Conversation.Container);
        }

        /// <summary>
        /// POST: api/Messages
        /// receive a message from a user and send replies
        /// </summary>
        /// <param name="activity"></param>
        [ResponseType(typeof(void))]
        public virtual async Task<HttpResponseMessage> Post([FromBody] Activity activity)
        {
            if (activity != null)
            {
                switch (activity.GetActivityType())
                {
                    case ActivityTypes.Message:
                        await Conversation.SendAsync(activity, () => new EchoDialog());
                        break;

                    case ActivityTypes.ConversationUpdate:
                        IConversationUpdateActivity update = activity;
                        // resolve the connector client from the container to make sure that it is 
                        // instantiated with the right MicrosoftAppCredentials
                        using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))
                        {
                            var client = scope.Resolve<IConnectorClient>();
                            if (update.MembersAdded.Any())
                            {
                                var reply = activity.CreateReply();
                                foreach (var newMember in update.MembersAdded)
                                {
                                    if (newMember.Id != activity.Recipient.Id)
                                    {
                                        reply.Text = $"Welcome {newMember.Name}!";
                                        await client.Conversations.ReplyToActivityAsync(reply);
                                    }
                                }
                            }
                        }
                        break;
                    case ActivityTypes.ContactRelationUpdate:
                    case ActivityTypes.Typing:
                    case ActivityTypes.DeleteUserData:
                    case ActivityTypes.Ping:
                    default:
                        Trace.TraceError($"Unknown activity type ignored: {activity.GetActivityType()}");
                        break;
                }
            }
            return new HttpResponseMessage(System.Net.HttpStatusCode.Accepted);
        }
    }
}

This is the key part showing, how you can update the Conversation.Container with the right MicrosoftAppCredentials:

var builder = new ContainerBuilder();
builder.Register(c => ((ClaimsIdentity)HttpContext.Current.User.Identity).GetCredentialsFromClaims())
           .AsSelf()
           .InstancePerLifetimeScope();
builder.Update(Conversation.Container);

I used the example above but HttpContext.Current was always null in the crucial bit of code:

            var builder = new ContainerBuilder();

            builder.Register(c => ((ClaimsIdentity)HttpContext.Current.User.Identity).GetCredentialsFromClaims())
                .AsSelf()
                .InstancePerLifetimeScope();
            builder.Update(Conversation.Container);

It turned out to be I was using .ConfigureAwait(false) throughout my MessagesController which caused the code above to execute on any thread (I've fallen into this pattern due to suffering deadlocks in the past).

The answer to this question (not bot related) explained precisely why I was getting null ref exceptions:
https://stackoverflow.com/questions/28427675/correct-way-to-use-httpcontext-current-user-with-async-await

I hope this helps someone else save half a day of their life :-)

Stu

What's the alternative to the .Update command now that it's depreciated?

I'm not an expert on this but from looking at the changes to the bot framework code made here https://github.com/Microsoft/BotBuilder-Samples/commit/34fc91f35dfbad56f8e1a662d036b3c6fd8355c0 I did this:

Conversation.UpdateContainer(builder => { builder.Register( c => ((ClaimsIdentity)HttpContext.Current.User.Identity).GetCredentialsFromClaims()) .AsSelf() .InstancePerLifetimeScope(); });

I have posted a question on Stack Overflow about this topic here.

The problem I'm facing is losing the HttpContext when using ConfigureAwait(false) so I then lose knowledge of which bot is currently executing. When I answered above about this I thought I'd solved it but I had just got lucky with changing a few ConfigureAwaits to true but inevitably it failed again in a different place in the code.

Would be great if anyone can give me some pointers.

Thanks

I've worked around this by storing the current 'bot id' in AppDomain data keyed by the activity.Conversation.Id in the first part of the Post method. I then moved the UpdateContainer code from a static constructor into the same Post method. This way the anonymous function can access the same local variables as in the main function which means I can get the credentials can access the activity.

@msft-shahins Hi, would you tell me please how do I know the appId when I am on the dialog or the messages controller? to have a distinct action depending on the bot. Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

stijnherreman picture stijnherreman  路  3Comments

sebsylvester picture sebsylvester  路  3Comments

jschristophe picture jschristophe  路  3Comments

vaditya04 picture vaditya04  路  3Comments

clearab picture clearab  路  3Comments