Skip to content
Merged
20 changes: 20 additions & 0 deletions libraries/Microsoft.Bot.Builder/BotAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ namespace Microsoft.Bot.Builder
public abstract class BotAdapter
{
public const string BotIdentityKey = "BotIdentity";
public const string OAuthScopeKey = "OAuthScope";

/// <summary>
/// Initializes a new instance of the <see cref="BotAdapter"/> class.
Expand Down Expand Up @@ -152,6 +153,25 @@ public virtual Task ContinueConversationAsync(ClaimsIdentity claimsIdentity, Con
throw new NotImplementedException();
}

/// <summary>
/// Sends a proactive message to a conversation.
/// </summary>
/// <param name="claimsIdentity">A <see cref="ClaimsIdentity"/> for the conversation.</param>
/// <param name="reference">A reference to the conversation to continue.</param>
/// <param name="audience">A value signifying the recipient of the proactive message.</param>
/// <param name="callback">The method to call for the resulting bot 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>Call this method to proactively send a message to a conversation.
/// Most _channels require a user to initiate a conversation with a bot
/// before the bot can send activities to the user.</remarks>
/// <seealso cref="RunPipelineAsync(ITurnContext, BotCallbackHandler, CancellationToken)"/>
public virtual Task ContinueConversationAsync(ClaimsIdentity claimsIdentity, ConversationReference reference, string audience, BotCallbackHandler callback, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}

/// <summary>
/// Creates a turn context and runs the middleware pipeline for an incoming TRUSTED activity.
/// </summary>
Expand Down
120 changes: 78 additions & 42 deletions libraries/Microsoft.Bot.Builder/BotFrameworkAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Integration;
using Microsoft.Bot.Builder.Skills;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
Expand Down Expand Up @@ -282,7 +283,9 @@ public override async Task ContinueConversationAsync(string botAppId, Conversati
new Claim(AuthenticationConstants.AppIdClaim, botAppId),
});

await ContinueConversationAsync(claimsIdentity, reference, callback, cancellationToken).ConfigureAwait(false);
var audience = GetBotFrameworkOAuthScope();

await ContinueConversationAsync(claimsIdentity, reference, audience, callback, cancellationToken).ConfigureAwait(false);
}

/// <summary>
Expand All @@ -307,12 +310,48 @@ public override async Task ContinueConversationAsync(string botAppId, Conversati
/// <seealso cref="BotAdapter.RunPipelineAsync(ITurnContext, BotCallbackHandler, CancellationToken)"/>
public override async Task ContinueConversationAsync(ClaimsIdentity claimsIdentity, ConversationReference reference, BotCallbackHandler callback, CancellationToken cancellationToken)
{
var audience = GetBotFrameworkOAuthScope();

await ContinueConversationAsync(claimsIdentity, reference, audience, callback, cancellationToken).ConfigureAwait(false);
}

public override async Task ContinueConversationAsync(ClaimsIdentity claimsIdentity, ConversationReference reference, string audience, BotCallbackHandler callback, CancellationToken cancellationToken)
{
if (claimsIdentity == null)
{
throw new ArgumentNullException(nameof(claimsIdentity));
}

if (reference == null)
{
throw new ArgumentNullException(nameof(reference));
}

if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}

if (string.IsNullOrWhiteSpace(audience))
{
throw new ArgumentNullException($"{nameof(audience)} cannot be null or white space.");
}

// Reusing the code from the above override, ContinueConversationAsync()
using (var context = new TurnContext(this, reference.GetContinuationActivity()))
{
context.TurnState.Add<IIdentity>(BotIdentityKey, claimsIdentity);
context.TurnState.Add<BotCallbackHandler>(callback);
await EnsureChannelConnectorClientIsCreatedAsync(reference.ServiceUrl, claimsIdentity, cancellationToken).ConfigureAwait(false);
var connectorClient = await CreateConnectorClientAsync(reference.ServiceUrl, claimsIdentity, cancellationToken).ConfigureAwait(false);

// Add audience to TurnContext.TurnState
context.TurnState.Add<string>(OAuthScopeKey, audience);

// Add the channel service URL to the trusted services list so we can send messages back.
// the service URL for skills is trusted because it is applied by the SkillHandler based on the original request
// received by the root bot
AppCredentials.TrustServiceUrl(reference.ServiceUrl);

var connectorClient = await CreateConnectorClientAsync(reference.ServiceUrl, claimsIdentity, audience, cancellationToken).ConfigureAwait(false);
context.TurnState.Add(connectorClient);

await RunPipelineAsync(context, callback, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -385,8 +424,22 @@ public override async Task<InvokeResponse> ProcessActivityAsync(ClaimsIdentity c
{
context.TurnState.Add<IIdentity>(BotIdentityKey, claimsIdentity);
context.TurnState.Add<BotCallbackHandler>(callback);

var connectorClient = await CreateConnectorClientAsync(activity.ServiceUrl, claimsIdentity, cancellationToken).ConfigureAwait(false);

// To create the correct cache key, provide the OAuthScope when calling CreateConnectorClientAsync.
// The OAuthScope is also stored on the TurnState to get the correct AppCredentials if fetching a token is required.
string scope;
if (!SkillValidation.IsSkillClaim(claimsIdentity.Claims))
{
scope = GetBotFrameworkOAuthScope();
}
else
{
// For activities received from another bot, the appropriate audience is obtained from the claims.
scope = JwtTokenValidation.GetAppIdFromClaims(claimsIdentity.Claims);
}

context.TurnState.Add(OAuthScopeKey, scope);
var connectorClient = await CreateConnectorClientAsync(activity.ServiceUrl, claimsIdentity, scope, cancellationToken).ConfigureAwait(false);
context.TurnState.Add(connectorClient);

await RunPipelineAsync(context, callback, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -484,8 +537,9 @@ public override async Task<ResourceResponse[]> SendActivitiesAsync(ITurnContext
try
{
var appId = GetBotAppId(turnContext);

_ = (await GetAppCredentialsAsync(appId).ConfigureAwait(false)).GetTokenAsync();

var oAuthScope = turnContext.TurnState.Get<string>(OAuthScopeKey);
_ = (await GetAppCredentialsAsync(appId, oAuthScope).ConfigureAwait(false)).GetTokenAsync();
}
catch (Exception ex)
{
Expand Down Expand Up @@ -1104,8 +1158,9 @@ protected virtual async Task<OAuthClient> CreateOAuthApiClientAsync(ITurnContext
var appId = GetBotAppId(turnContext);

var clientKey = $"{appId}:{oAuthAppCredentials?.MicrosoftAppId}";
var oAuthScope = turnContext.TurnState.Get<string>(OAuthScopeKey);

var appCredentials = oAuthAppCredentials ?? await GetAppCredentialsAsync(appId).ConfigureAwait(false);
var appCredentials = oAuthAppCredentials ?? await GetAppCredentialsAsync(appId, oAuthScope).ConfigureAwait(false);

if (!OAuthClientConfig.EmulateOAuthCards &&
string.Equals(turnContext.Activity.ChannelId, Channels.Emulator, StringComparison.InvariantCultureIgnoreCase) &&
Expand Down Expand Up @@ -1202,7 +1257,7 @@ protected virtual async Task<AppCredentials> BuildCredentialsAsync(string appId,
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>ConnectorClient instance.</returns>
/// <exception cref="NotSupportedException">ClaimsIdentity cannot be null. Pass Anonymous ClaimsIdentity if authentication is turned off.</exception>
private async Task<IConnectorClient> CreateConnectorClientAsync(string serviceUrl, ClaimsIdentity claimsIdentity, CancellationToken cancellationToken)
private async Task<IConnectorClient> CreateConnectorClientAsync(string serviceUrl, ClaimsIdentity claimsIdentity, string audience, CancellationToken cancellationToken = default)
{
if (claimsIdentity == null)
{
Expand All @@ -1223,11 +1278,14 @@ private async Task<IConnectorClient> CreateConnectorClientAsync(string serviceUr
if (botAppIdClaim != null)
{
var botId = botAppIdClaim.Value;
string scope = null;
if (SkillValidation.IsSkillClaim(claimsIdentity.Claims))
var scope = audience;

if (string.IsNullOrWhiteSpace(audience))
{
// The skill connector has the target skill in the OAuthScope.
scope = JwtTokenValidation.GetAppIdFromClaims(claimsIdentity.Claims);
scope = SkillValidation.IsSkillClaim(claimsIdentity.Claims) ?
JwtTokenValidation.GetAppIdFromClaims(claimsIdentity.Claims) :
GetBotFrameworkOAuthScope();
}

appCredentials = await GetAppCredentialsAsync(botId, scope, cancellationToken).ConfigureAwait(false);
Expand All @@ -1244,7 +1302,8 @@ private async Task<IConnectorClient> CreateConnectorClientAsync(string serviceUr
/// <returns>Connector client instance.</returns>
private IConnectorClient CreateConnectorClient(string serviceUrl, AppCredentials appCredentials = null)
{
var clientKey = $"{serviceUrl}{appCredentials?.MicrosoftAppId ?? string.Empty}";
// As multiple bots can listen on a single serviceUrl, the clientKey also includes the OAuthScope.
var clientKey = $"{serviceUrl}{appCredentials?.MicrosoftAppId}:{appCredentials?.OAuthScope}";

return _connectorClients.GetOrAdd(clientKey, (key) =>
{
Expand Down Expand Up @@ -1278,7 +1337,7 @@ private IConnectorClient CreateConnectorClient(string serviceUrl, AppCredentials
/// <param name="oAuthScope">The scope for the token. Skills use the skill's app ID. </param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>App credentials.</returns>
private async Task<AppCredentials> GetAppCredentialsAsync(string appId, string oAuthScope = null, CancellationToken cancellationToken = default)
private async Task<AppCredentials> GetAppCredentialsAsync(string appId, string oAuthScope, CancellationToken cancellationToken = default)
{
if (appId == null)
{
Expand Down Expand Up @@ -1330,36 +1389,13 @@ private string GetBotAppId(ITurnContext turnContext)
}

/// <summary>
/// This method creates a default ConnectorClient for the bot AppId and also registers the service URL as a trusted URL.
/// This method returns the correct Bot Framework OAuthScope for AppCredentials.
/// </summary>
/// <remarks>
/// When a parent bot is deployed to multiple instances the cache AppIds, ConnectClients and Trusted URL are not initialized
/// if the server instance hasn't been hit by a request from the channel.
/// This code ensures that the required objects are created.
/// </remarks>
private async Task EnsureChannelConnectorClientIsCreatedAsync(string serviceUrl, ClaimsIdentity claimsIdentity, CancellationToken cancellationToken)
private string GetBotFrameworkOAuthScope()
{
// Ensure we have a default ConnectorClient and MSAppCredentials instance for the audience.
var audience = claimsIdentity.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.AudienceClaim)?.Value;
if (string.IsNullOrWhiteSpace(audience) || !AuthenticationConstants.ToBotFromChannelTokenIssuer.Equals(audience, StringComparison.InvariantCultureIgnoreCase))
{
// We create a default connector for audiences that are not coming from the default https://api.botframework.com audience.
// We create a default claim that contains only the desired audience.
var defaultConnectorClaims = new List<Claim> { new Claim(AuthenticationConstants.AudienceClaim, audience) };
var connectorClaimsIdentity = new ClaimsIdentity(defaultConnectorClaims);

// The CreateConnectorClientAsync will create a ConnectorClient with an associated MicrosoftAppId for that claim and will
// initialize the dictionaries that contain the cache instances.
await CreateConnectorClientAsync(serviceUrl, connectorClaimsIdentity, cancellationToken).ConfigureAwait(false);
}

if (SkillValidation.IsSkillClaim(claimsIdentity.Claims))
{
// Add the channel service URL to the trusted services list so we can send messages back.
// the service URL for skills is trusted because it is applied by the SkillHandler based on the original request
// received by the root bot
AppCredentials.TrustServiceUrl(serviceUrl);
}
return ChannelProvider != null && ChannelProvider.IsGovernment() ?
GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope :
AuthenticationConstants.ToChannelFromBotOAuthScope;
}

/// <summary>
Expand Down
Loading