Skip to content

Commit

Permalink
Update Facebook, Slack, Webex adapters (#4214)
Browse files Browse the repository at this point in the history
* Update Facebook, Slack, Webex adapters to follow pattern from Twilio adapter

* Tweak exception messages
  • Loading branch information
garypretty authored Jul 1, 2020
1 parent abeab29 commit a6a1e92
Show file tree
Hide file tree
Showing 20 changed files with 382 additions and 969 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,24 @@ public class FacebookAdapter : BotAdapter, IBotFrameworkHttpAdapter
/// - `AppSecret`, the secret used to validate incoming webhooks.
/// - `AccessToken`, an access token for the bot.
/// </remarks>
/// <param name="options">An instance of <see cref="FacebookAdapterOptions"/>.</param>
/// <param name="logger">The logger this adapter should use.</param>
public FacebookAdapter(IConfiguration configuration, ILogger logger = null)
: this(new FacebookAdapterOptions(configuration["FacebookVerifyToken"], configuration["FacebookAppSecret"], configuration["FacebookAccessToken"]), logger)
public FacebookAdapter(IConfiguration configuration, FacebookAdapterOptions options = null, ILogger logger = null)
: this(new FacebookClientWrapper(new FacebookClientWrapperOptions(configuration["FacebookVerifyToken"], configuration["FacebookAppSecret"], configuration["FacebookAccessToken"])), options, logger)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="FacebookAdapter"/> class using an existing Facebook client.
/// </summary>
/// /// <param name="facebookClient">Client used to interact with the Facebook API.</param>
/// <param name="options">Options for the Facebook Adapter.</param>
/// <param name="logger">The logger this adapter should use.</param>
/// <param name="facebookClient">Client used to interact with the Facebook API.</param>
/// <exception cref="ArgumentNullException"><paramref name="options"/> is null.</exception>
public FacebookAdapter(FacebookAdapterOptions options, ILogger logger = null, FacebookClientWrapper facebookClient = null)
public FacebookAdapter(FacebookClientWrapper facebookClient, FacebookAdapterOptions options, ILogger logger = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_facebookClient = facebookClient ?? new FacebookClientWrapper(options);
_options = options ?? new FacebookAdapterOptions();
_facebookClient = facebookClient ?? throw new ArgumentNullException(nameof(facebookClient));
_logger = logger ?? NullLogger.Instance;
}

Expand Down Expand Up @@ -241,7 +242,7 @@ public async Task ProcessAsync(HttpRequest httpRequest, HttpResponse httpRespons
if (!_facebookClient.VerifySignature(httpRequest, stringifiedBody) && _options.VerifyIncomingRequests)
{
await FacebookHelper.WriteAsync(httpResponse, HttpStatusCode.Unauthorized, string.Empty, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
throw new AuthenticationException("WARNING: Webhook received message with invalid signature. Potential malicious behavior!");
throw new AuthenticationException("Webhook received message with invalid signature. Potential malicious behavior!");
}

FacebookResponseEvent facebookResponseEvent = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,79 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Threading.Tasks;

namespace Microsoft.Bot.Builder.Adapters.Facebook
{
/// <summary>
/// Options class for Facebook Adapter.
/// </summary>
public class FacebookAdapterOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="FacebookAdapterOptions"/> class.
/// </summary>
/// <param name="verifyToken">The token used to validate that incoming webhooks are originated from Facebook.</param>
/// <param name="appSecret">The app secret.</param>
/// <param name="accessToken">The Facebook access token.</param>
/// <param name="apiHost">A token for validating the origin of incoming webhooks.</param>
/// <param name="apiVersion">A token for a bot to work on a single workspace.</param>
public FacebookAdapterOptions(string verifyToken, string appSecret, string accessToken, string apiHost = "graph.facebook.com", string apiVersion = "v3.2")
{
FacebookVerifyToken = verifyToken;
FacebookAppSecret = appSecret;
FacebookAccessToken = accessToken;
FacebookApiHost = apiHost;
FacebookApiVersion = apiVersion;
}

/// <summary>
/// Gets or sets the alternate root URL used to construct calls to Facebook's API. Defaults to "graph.facebook.com" but can be changed (for mocking, proxy, etc).
/// </summary>
/// <value>The API host.</value>
public string FacebookApiHost { get; set; }

/// <summary>
/// Gets or sets the alternate API version used to construct calls to Facebook's API. Defaults to "v3.2".
/// </summary>
/// <value>The API version.</value>
public string FacebookApiVersion { get; set; }

/// <summary>
/// Gets or sets the verify token used to initially create and verify the webhooks subscription settings on Facebook's developer portal.
/// </summary>
/// <value>The verify token.</value>
public string FacebookVerifyToken { get; set; }

/// <summary>
/// Gets or sets the app secret from the **Basic Settings** page from your app's configuration in the Facebook developer portal.
/// </summary>
/// <value>The app secret.</value>
public string FacebookAppSecret { get; set; }

/// <summary>
/// Gets or sets the access token.
/// When bound to a single page, use `access_token` to specify the "page access token" provided in the Facebook developer portal's "Access Tokens" widget of the "Messenger Settings" page.
/// </summary>
/// <value>The access token.</value>
public string FacebookAccessToken { get; set; }

/// <summary>
/// Gets or sets a value indicating whether incoming requests should be verified.
/// Should be set to true in Production but can be set to false for testing or development purposes.
/// </summary>
/// <value>The flag to indicate if incoming requests should be verified.</value>
public bool VerifyIncomingRequests { get; set; } = true;

/// <summary>
/// Throws a <see cref="NotImplementedException"/> exception in all cases.
/// </summary>
/// <param name="pageId">The page ID.</param>
/// <returns>The access token for the page.</returns>
public virtual Task<string> GetAccessTokenForPageAsync(string pageId)
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook
public class FacebookClientWrapper
{
/// <summary>
/// An instance of the FacebookAdapterOptions class.
/// An instance of the FacebookClientWrapperOptions class.
/// </summary>
private readonly FacebookAdapterOptions _options;
private readonly FacebookClientWrapperOptions _options;

/// <summary>
/// Initializes a new instance of the <see cref="FacebookClientWrapper"/> class.
/// </summary>
/// <param name="options">An object containing API credentials, a webhook verification token, and other options.</param>
public FacebookClientWrapper(FacebookAdapterOptions options)
public FacebookClientWrapper(FacebookClientWrapperOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Threading.Tasks;

namespace Microsoft.Bot.Builder.Adapters.Facebook
{
/// <summary>
/// Options class for Facebook Adapter.
/// </summary>
public class FacebookClientWrapperOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="FacebookClientWrapperOptions"/> class.
/// </summary>
/// <param name="verifyToken">The token used to validate that incoming webhooks are originated from Facebook.</param>
/// <param name="appSecret">The app secret.</param>
/// <param name="accessToken">The Facebook access token.</param>
/// <param name="apiHost">A token for validating the origin of incoming webhooks.</param>
/// <param name="apiVersion">A token for a bot to work on a single workspace.</param>
public FacebookClientWrapperOptions(string verifyToken, string appSecret, string accessToken, string apiHost = "graph.facebook.com", string apiVersion = "v3.2")
{
FacebookVerifyToken = verifyToken;
FacebookAppSecret = appSecret;
FacebookAccessToken = accessToken;
FacebookApiHost = apiHost;
FacebookApiVersion = apiVersion;
}

/// <summary>
/// Gets or sets the alternate root URL used to construct calls to Facebook's API. Defaults to "graph.facebook.com" but can be changed (for mocking, proxy, etc).
/// </summary>
/// <value>The API host.</value>
public string FacebookApiHost { get; set; }

/// <summary>
/// Gets or sets the alternate API version used to construct calls to Facebook's API. Defaults to "v3.2".
/// </summary>
/// <value>The API version.</value>
public string FacebookApiVersion { get; set; }

/// <summary>
/// Gets or sets the verify token used to initially create and verify the webhooks subscription settings on Facebook's developer portal.
/// </summary>
/// <value>The verify token.</value>
public string FacebookVerifyToken { get; set; }

/// <summary>
/// Gets or sets the app secret from the **Basic Settings** page from your app's configuration in the Facebook developer portal.
/// </summary>
/// <value>The app secret.</value>
public string FacebookAppSecret { get; set; }

/// <summary>
/// Gets or sets the access token.
/// When bound to a single page, use `access_token` to specify the "page access token" provided in the Facebook developer portal's "Access Tokens" widget of the "Messenger Settings" page.
/// </summary>
/// <value>The access token.</value>
public string FacebookAccessToken { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Authentication;
using System.Security.Claims;
using System.Security.Principal;
using System.Text;
Expand All @@ -27,6 +28,7 @@ public class SlackAdapter : BotAdapter, IBotFrameworkHttpAdapter
{
private readonly SlackClientWrapper _slackClient;
private readonly ILogger _logger;
private readonly SlackAdapterOptions _options;

/// <summary>
/// Initializes a new instance of the <see cref="SlackAdapter"/> class using configuration settings.
Expand All @@ -38,9 +40,10 @@ public class SlackAdapter : BotAdapter, IBotFrameworkHttpAdapter
/// SlackBotToken: A token for a bot to work on a single workspace.
/// SlackClientSigningSecret: The token used to validate that incoming webhooks are originated from Slack.
/// </remarks>
/// <param name="options">An instance of <see cref="SlackAdapterOptions"/>.</param>
/// <param name="logger">The ILogger implementation this adapter should use.</param>
public SlackAdapter(IConfiguration configuration, ILogger logger = null)
: this(new SlackAdapterOptions(configuration["SlackVerificationToken"], configuration["SlackBotToken"], configuration["SlackClientSigningSecret"]), logger)
public SlackAdapter(IConfiguration configuration, SlackAdapterOptions options = null, ILogger logger = null)
: this(new SlackClientWrapper(new SlackClientWrapperOptions(configuration["SlackVerificationToken"], configuration["SlackBotToken"], configuration["SlackClientSigningSecret"])), options, logger)
{
}

Expand All @@ -51,10 +54,11 @@ public SlackAdapter(IConfiguration configuration, ILogger logger = null)
/// <param name="adapterOptions">The adapter options to be used when connecting to the Slack API.</param>
/// <param name="logger">The ILogger implementation this adapter should use.</param>
/// <param name="slackClient">The SlackClientWrapper used to connect to the Slack API.</param>
public SlackAdapter(SlackAdapterOptions adapterOptions, ILogger logger = null, SlackClientWrapper slackClient = null)
public SlackAdapter(SlackClientWrapper slackClient, SlackAdapterOptions adapterOptions, ILogger logger = null)
{
_slackClient = slackClient ?? new SlackClientWrapper(adapterOptions) ?? throw new ArgumentNullException(nameof(adapterOptions));
_slackClient = slackClient ?? throw new ArgumentNullException(nameof(adapterOptions));
_logger = logger ?? NullLogger.Instance;
_options = adapterOptions ?? new SlackAdapterOptions();
}

/// <summary>
Expand Down Expand Up @@ -140,9 +144,9 @@ public override async Task<ResourceResponse> UpdateActivityAsync(ITurnContext tu

var results = await _slackClient.UpdateAsync(message.Ts, message.Channel, message.Text, cancellationToken: cancellationToken).ConfigureAwait(false);

if (!results.ok)
if (!results.Ok)
{
throw new Exception($"Error updating activity on Slack:{results}");
throw new InvalidOperationException($"Error updating activity on Slack:{results}");
}

return new ResourceResponse()
Expand Down Expand Up @@ -271,11 +275,11 @@ public async Task ProcessAsync(HttpRequest request, HttpResponse response, IBot
body = await sr.ReadToEndAsync().ConfigureAwait(false);
}

if (!_slackClient.VerifySignature(request, body))
if (_options.VerifyIncomingRequests && !_slackClient.VerifySignature(request, body))
{
const string text = "Rejected due to mismatched header signature";
await SlackHelper.WriteAsync(response, HttpStatusCode.Unauthorized, text, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
throw new Exception(text);
throw new AuthenticationException(text);
}

var requestContentType = request.Headers["Content-Type"].ToString();
Expand All @@ -293,7 +297,7 @@ public async Task ProcessAsync(HttpRequest request, HttpResponse response, IBot
{
var serializedPayload = JsonConvert.SerializeObject(postValues);
var payload = JsonConvert.DeserializeObject<CommandPayload>(serializedPayload);
activity = await SlackHelper.CommandToActivityAsync(payload, _slackClient, cancellationToken).ConfigureAwait(false);
activity = SlackHelper.CommandToActivity(payload, _slackClient);
}
}
else if (requestContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
Expand All @@ -311,14 +315,14 @@ public async Task ProcessAsync(HttpRequest request, HttpResponse response, IBot
{
var text = $"Rejected due to mismatched verificationToken:{bodyObject["token"]}";
await SlackHelper.WriteAsync(response, HttpStatusCode.Forbidden, text, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
throw new Exception(text);
throw new AuthenticationException(text);
}

if (bodyObject["type"]?.ToString() == "event_callback")
{
// this is an event api post
var eventRequest = bodyObject.ToObject<EventRequest>();
activity = await SlackHelper.EventToActivityAsync(eventRequest, _slackClient, cancellationToken).ConfigureAwait(false);
activity = SlackHelper.EventToActivity(eventRequest, _slackClient);
}
}

Expand Down
Loading

0 comments on commit a6a1e92

Please sign in to comment.