Skip to content

Commit

Permalink
Merge 4f5a91e into 6955e38
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeffders committed Feb 8, 2020
2 parents 6955e38 + 4f5a91e commit 6c9b983
Show file tree
Hide file tree
Showing 23 changed files with 1,761 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ private async Task SendMessageAsync(string message)
{
var parameters = GetParameters(message);
var signature = ComputeSignature(parameters);
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Twilio-Signature", signature);
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("X-Twilio-Signature", signature);

await client.PostAsync(_botEndpoint, new FormUrlEncodedContent(parameters));
await client.PostAsync(_botEndpoint, new FormUrlEncodedContent(parameters));
}
}

private string ComputeSignature(IDictionary<string, string> parameters)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,12 +504,12 @@ void Update()
if (value is object obj)
{
// For an object we need to see if any children path are being tracked
void CheckChildren(string property, object value)
void CheckChildren(string property, object instance)
{
// Add new child segment
trackedPath += "_" + property.ToLower();
Update();
if (value is object child)
if (instance is object child)
{
ObjectPath.ForEachProperty(child, CheckChildren);
}
Expand Down
147 changes: 130 additions & 17 deletions libraries/Microsoft.Bot.Builder.Dialogs/Prompts/OAuthPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Text.RegularExpressions;
using System.Threading;
Expand Down Expand Up @@ -272,7 +273,13 @@ private static bool IsTokenResponseEvent(ITurnContext turnContext)
private static bool IsTeamsVerificationInvoke(ITurnContext turnContext)
{
var activity = turnContext.Activity;
return activity.Type == ActivityTypes.Invoke && activity.Name == "signin/verifyState";
return activity.Type == ActivityTypes.Invoke && activity.Name == SignInOperations.VerifyStateOperationName;
}

private static bool IsTokenExchangeRequestInvoke(ITurnContext turnContext)
{
var activity = turnContext.Activity;
return activity.Type == ActivityTypes.Invoke && activity.Name == SignInOperations.TokenExchangeOperationName;
}

private static bool ChannelSupportsOAuthCard(string channelId)
Expand All @@ -293,7 +300,7 @@ private async Task SendOAuthCardAsync(ITurnContext turnContext, IMessageActivity
{
BotAssert.ContextNotNull(turnContext);

if (!(turnContext.Adapter is ICredentialTokenProvider adapter))
if (!(turnContext.Adapter is ITokenExchangeProvider adapter))
{
throw new InvalidOperationException("OAuthPrompt.Prompt(): not supported by the current adapter");
}
Expand All @@ -314,7 +321,7 @@ private async Task SendOAuthCardAsync(ITurnContext turnContext, IMessageActivity
{
if (!prompt.Attachments.Any(a => a.Content is SigninCard))
{
var link = await adapter.GetOauthSignInLinkAsync(turnContext, _settings.OAuthAppCredentials, _settings.ConnectionName, cancellationToken).ConfigureAwait(false);
var signInResource = await adapter.GetSignInResourceAsync(turnContext, _settings.ConnectionName, cancellationToken).ConfigureAwait(false);
prompt.Attachments.Add(new Attachment
{
ContentType = SigninCard.ContentType,
Expand All @@ -326,7 +333,7 @@ private async Task SendOAuthCardAsync(ITurnContext turnContext, IMessageActivity
new CardAction
{
Title = _settings.Title,
Value = link,
Value = signInResource.SignInLink,
Type = ActionTypes.Signin,
},
},
Expand All @@ -337,18 +344,18 @@ private async Task SendOAuthCardAsync(ITurnContext turnContext, IMessageActivity
else if (!prompt.Attachments.Any(a => a.Content is OAuthCard))
{
var cardActionType = ActionTypes.Signin;
string signInLink = null;
var signInResource = await adapter.GetSignInResourceAsync(turnContext, _settings.ConnectionName, cancellationToken).ConfigureAwait(false);
var value = signInResource.SignInLink;

if (turnContext.Activity.IsFromStreamingConnection())
{
signInLink = await adapter.GetOauthSignInLinkAsync(turnContext, _settings.OAuthAppCredentials, _settings.ConnectionName, cancellationToken).ConfigureAwait(false);
}
else if (turnContext.TurnState.Get<ClaimsIdentity>("BotIdentity") is ClaimsIdentity botIdentity && SkillValidation.IsSkillClaim(botIdentity.Claims))
if (turnContext.TurnState.Get<ClaimsIdentity>("BotIdentity") is ClaimsIdentity botIdentity && SkillValidation.IsSkillClaim(botIdentity.Claims))
{
// Force magic code for Skills (to be addressed in R8)
signInLink = await adapter.GetOauthSignInLinkAsync(turnContext, _settings.ConnectionName, cancellationToken).ConfigureAwait(false);
cardActionType = ActionTypes.OpenUrl;
}
else
{
value = null;
}

prompt.Attachments.Add(new Attachment
{
Expand All @@ -364,10 +371,11 @@ private async Task SendOAuthCardAsync(ITurnContext turnContext, IMessageActivity
Title = _settings.Title,
Text = _settings.Text,
Type = cardActionType,
Value = signInLink
}
}
}
Value = value
},
},
TokenExchangeResource = signInResource.TokenExchangeResource,
},
});
}

Expand Down Expand Up @@ -426,12 +434,103 @@ private async Task<PromptRecognizerResult<TokenResponse>> RecognizeTokenAsync(IT
}
else
{
await turnContext.SendActivityAsync(new Activity { Type = ActivityTypesEx.InvokeResponse, Value = new InvokeResponse { Status = 404 } }, cancellationToken).ConfigureAwait(false);
await this.SendInvokeResponseAsync(turnContext, cancellationToken, HttpStatusCode.NotFound).ConfigureAwait(false);
}
}
catch
{
await turnContext.SendActivityAsync(new Activity { Type = ActivityTypesEx.InvokeResponse, Value = new InvokeResponse { Status = 500 } }, cancellationToken).ConfigureAwait(false);
await this.SendInvokeResponseAsync(turnContext, cancellationToken, HttpStatusCode.InternalServerError).ConfigureAwait(false);
}
}
else if (IsTokenExchangeRequestInvoke(turnContext))
{
var tokenExchangeRequest = ((JObject)turnContext.Activity.Value)?.ToObject<TokenExchangeInvokeRequest>();

if (tokenExchangeRequest == null)
{
await this.SendInvokeResponseAsync(
turnContext,
cancellationToken,
HttpStatusCode.BadRequest,
new TokenExchangeInvokeResponse()
{
Id = null,
ConnectionName = _settings.ConnectionName,
FailureDetail = "The bot received an InvokeActivity that is missing a TokenExchangeInvokeRequest value. This is required to be sent with the InvokeActivity.",
}).ConfigureAwait(false);
}
else if (tokenExchangeRequest.ConnectionName != _settings.ConnectionName)
{
await this.SendInvokeResponseAsync(
turnContext,
cancellationToken,
HttpStatusCode.BadRequest,
new TokenExchangeInvokeResponse()
{
Id = tokenExchangeRequest.Id,
ConnectionName = _settings.ConnectionName,
FailureDetail = "The bot received an InvokeActivity with a TokenExchangeInvokeRequest containing a ConnectionName that does not match the ConnectionName expected by the bot's active OAuthPrompt. Ensure these names match when sending the InvokeActivityInvalid ConnectionName in the TokenExchangeInvokeRequest",
}).ConfigureAwait(false);
}
else if (!(turnContext.Adapter is ITokenExchangeProvider adapter))
{
await this.SendInvokeResponseAsync(
turnContext,
cancellationToken,
HttpStatusCode.BadGateway,
new TokenExchangeInvokeResponse()
{
Id = tokenExchangeRequest.Id,
ConnectionName = _settings.ConnectionName,
FailureDetail = "The bot's BotAdapter does not support token exchange operations. Ensure the bot's Adapter supports the ITokenExchangeProvider interface.",
}).ConfigureAwait(false);
throw new InvalidOperationException("OAuthPrompt.Recognize(): not supported by the current adapter");
}
else
{
var tokenExchangeResponse = await adapter.ExchangeTokenAsync(
turnContext,
_settings.ConnectionName,
turnContext.Activity.From.Id,
new TokenExchangeRequest()
{
Token = tokenExchangeRequest.Token,
},
cancellationToken).ConfigureAwait(false);

if (tokenExchangeResponse == null || string.IsNullOrEmpty(tokenExchangeResponse.Token))
{
await this.SendInvokeResponseAsync(
turnContext,
cancellationToken,
HttpStatusCode.Conflict,
new TokenExchangeInvokeResponse()
{
Id = tokenExchangeRequest.Id,
ConnectionName = _settings.ConnectionName,
FailureDetail = "The bot is unable to exchange token. Proceed with regular login.",
}).ConfigureAwait(false);
}
else
{
await this.SendInvokeResponseAsync(
turnContext,
cancellationToken,
HttpStatusCode.OK,
new TokenExchangeInvokeResponse()
{
Id = tokenExchangeRequest.Id,
ConnectionName = _settings.ConnectionName,
}).ConfigureAwait(false);

result.Succeeded = true;
result.Value = new TokenResponse()
{
ChannelId = tokenExchangeResponse.ChannelId,
ConnectionName = tokenExchangeResponse.ConnectionName,
Token = tokenExchangeResponse.Token,
};
}
}
}
else if (turnContext.Activity.Type == ActivityTypes.Message)
Expand All @@ -455,5 +554,19 @@ private async Task<PromptRecognizerResult<TokenResponse>> RecognizeTokenAsync(IT

return result;
}

private async Task SendInvokeResponseAsync(ITurnContext turnContext, CancellationToken cancellationToken, HttpStatusCode statusCode, object body = null)
{
await turnContext.SendActivityAsync(
new Activity
{
Type = ActivityTypesEx.InvokeResponse,
Value = new InvokeResponse
{
Status = (int)statusCode,
Body = body,
},
}, cancellationToken).ConfigureAwait(false);
}
}
}
83 changes: 83 additions & 0 deletions libraries/Microsoft.Bot.Builder/ActivityHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public virtual Task OnTurnAsync(ITurnContext turnContext, CancellationToken canc
case ActivityTypes.Event:
return OnEventActivityAsync(new DelegatingTurnContext<IEventActivity>(turnContext), cancellationToken);

case ActivityTypes.Invoke:
return OnInvokeActivityAsync(new DelegatingTurnContext<IInvokeActivity>(turnContext), cancellationToken);

case ActivityTypes.EndOfConversation:
return OnEndOfConversationActivityAsync(new DelegatingTurnContext<IEndOfConversationActivity>(turnContext), cancellationToken);

Expand Down Expand Up @@ -380,6 +383,86 @@ protected virtual Task OnEventAsync(ITurnContext<IEventActivity> turnContext, Ca
return Task.CompletedTask;
}

/// <summary>
/// Invoked when an invoke activity is received from the connector when the base behavior of
/// <see cref="OnTurnAsync(ITurnContext, CancellationToken)"/> is used.
/// Invoke activities can be used to communicate many different things.
/// By default, this method will call <see cref="OnSignInInvokeAsync(ITurnContext{IInvokeActivity}, CancellationToken)"/> if the
/// activity's name is <c>signin/verifyState</c> or <c>signin/tokenExchange</c>.
/// A <c>signin/verifyState</c> or <c>signin/tokenExchange</c> invoke can be triggered by an <see cref="OAuthCard"/>.
/// </summary>
/// <param name="turnContext">A strongly-typed context object for this turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>
/// When the <see cref="OnTurnAsync(ITurnContext, CancellationToken)"/>
/// method receives an invoke activity, it calls this method.
/// If the event <see cref="IInvokeActivity.Name"/> is `signin/verifyState` or `signin/tokenExchange`, it calls
/// <see cref="OnSignInInvokeAsync(ITurnContext{IInvokeActivity}, CancellationToken)"/>
/// Invoke activities communicate programmatic commands from a client or channel to a bot.
/// The meaning of an invoke activity is defined by the <see cref="IInvokeActivity.Name"/> property,
/// which is meaningful within the scope of a channel.
/// A `signin/verifyState` or `signin/tokenExchange` invoke can be triggered by an <see cref="OAuthCard"/> or an OAuth prompt.
/// </remarks>
/// <seealso cref="OnTurnAsync(ITurnContext, CancellationToken)"/>
protected virtual Task OnInvokeActivityAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
if (turnContext.Activity.Name == SignInOperations.VerifyStateOperationName || turnContext.Activity.Name == SignInOperations.TokenExchangeOperationName)
{
return OnSignInInvokeAsync(turnContext, cancellationToken);
}

return OnInvokeAsync(turnContext, cancellationToken);
}

/// <summary>
/// Invoked when a <c>signin/verifyState</c> or <c>signin/tokenExchange</c> event is received when the base behavior of
/// <see cref="OnInvokeActivityAsync(ITurnContext{IInvokeActivity}, CancellationToken)"/> is used.
/// If using an <c>OAuthPrompt</c>, override this method to forward this <see cref="Activity"/> to the current dialog.
/// By default, this method does nothing.
/// </summary>
/// <param name="turnContext">A strongly-typed context object for this turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>
/// When the <see cref="OnInvokeActivityAsync(ITurnContext{IInvokeActivity}, CancellationToken)"/>
/// method receives an Invoke with a <see cref="IInvokeActivity.Name"/> of `tokens/response`,
/// it calls this method.
///
/// If your bot uses the <c>OAuthPrompt</c>, forward the incoming <see cref="Activity"/> to
/// the current dialog.
/// </remarks>
/// <seealso cref="OnInvokeActivityAsync(ITurnContext{IInvokeActivity}, CancellationToken)"/>
/// <seealso cref="OnInvokeAsync(ITurnContext{IInvokeActivity}, CancellationToken)"/>
protected virtual Task OnSignInInvokeAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

/// <summary>
/// Invoked when an Invoke other than <c>signin/verifyState</c> or <c>signin/tokenExchange</c> is received when the base behavior of
/// <see cref="OnInvokeActivityAsync(ITurnContext{IInvokeActivity}, CancellationToken)"/> is used.
/// This method could optionally be overridden if the bot is meant to handle miscellaneous Invokes.
/// By default, this method does nothing.
/// </summary>
/// <param name="turnContext">A strongly-typed context object for this turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>
/// When the <see cref="OnInvokeActivityAsync(ITurnContext{IInvokeActivity}, CancellationToken)"/>
/// method receives an Invoke with a <see cref="IInvokeActivity.Name"/> other than `tokens/response`,
/// it calls this method.
/// </remarks>
/// <seealso cref="OnInvokeActivityAsync(ITurnContext{IInvokeActivity}, CancellationToken)"/>
/// <seealso cref="OnSignInInvokeAsync(ITurnContext{IInvokeActivity}, CancellationToken)"/>
protected virtual Task OnInvokeAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

/// <summary>
/// Override this in a derived class to provide logic specific to
/// <see cref="ActivityTypes.EndOfConversation"/> activities, such as the conversational logic.
Expand Down

0 comments on commit 6c9b983

Please sign in to comment.