diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c3fd3e836b..4d4e072c6f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -20,50 +20,50 @@ /build/** @microsoft/bb-dotnet # Specific Test Projects -/tests/Microsoft.Bot.Builder.TestBot.Json/** @chrimc62 @tomlm @microsoft/bf-adaptive +/tests/Microsoft.Bot.Builder.TestBot.Json/** @chrimc62 @tomlm @microsoft/bf-adaptive @EricDahlvang @mrivera-ms /tests/Microsoft.Bot.Builder.TestProtocol/** @gabog @microsoft/bf-skills # Adapters -/libraries/Adapters/** @garypretty @mrivera-ms -/tests/Adapters/** @garypretty @mrivera-ms +/libraries/Adapters/** @EricDahlvang @garypretty @mrivera-ms +/tests/Adapters/** @EricDahlvang @garypretty @mrivera-ms # Platform Integration Libaries (.NET Core and WebApi) /libraries/integration/** @microsoft/bb-dotnet-integration /tests/integration/** @microsoft/bb-dotnet-integration # Application Insights/Telemetry -/libraries/Microsoft.Bot.Builder.ApplicationInsights/** @garypretty @mrivera-ms -/tests/Microsoft.Bot.Builder.ApplicationInsights.Tests/** @garypretty @mrivera-ms +/libraries/Microsoft.Bot.Builder.ApplicationInsights/** @EricDahlvang @johnataylor @mrivera-ms +/tests/Microsoft.Bot.Builder.ApplicationInsights.Tests/** @EricDahlvang @johnataylor @mrivera-ms # AI: LUIS + Orchestrator /libraries/Microsoft.Bot.Builder.AI*/** @microsoft/bf-cog-services /tests/Microsoft.Bot.Builder.AI*/** @microsoft/bf-cog-services # AI: QnA Maker -/libraries/Microsoft.Bot.Builder.AI.QnA/** @mrivera-ms @microsoft/bf-cog-services -/tests/Microsoft.Bot.Builder.AI.QnA.Tests/** @mrivera-ms @microsoft/bf-cog-services +/libraries/Microsoft.Bot.Builder.AI.QnA/** @mrivera-ms @microsoft/bf-cog-services +/tests/Microsoft.Bot.Builder.AI.QnA.Tests/** @mrivera-ms @microsoft/bf-cog-services # Azure (Storage) /libraries/Microsoft.Bot.Builder.Azure/** @EricDahlvang @mrivera-ms /tests/Microsoft.Bot.Builder.Azure.Tests/** @EricDahlvang @mrivera-ms # Adaptive Dialogs -/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/** @microsoft/bf-adaptive -/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Teams/** @microsoft/bf-adaptive -/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Testing/** @microsoft/bf-adaptive +/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms +/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Teams/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms +/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive.Testing/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms # Adaptive Dialogs' tests -/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Profiling/** @microsoft/bf-adaptive -/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Teams.Tests/** @microsoft/bf-adaptive -/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Templates.Tests/** @microsoft/bf-adaptive -/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/** @microsoft/bf-adaptive +/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Profiling/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms +/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Teams.Tests/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms +/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Templates.Tests/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms +/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms # AdaptiveExpressions & LanguageGeneration libraries -/libraries/Microsoft.Bot.Builder.Dialogs.Declarative/** @microsoft/bf-adaptive -/tests/Microsoft.Bot.Builder.Dialogs.Declarative.Tests/** @microsoft/bf-adaptive -/libraries/Microsoft.Bot.Builder.LanguageGeneration/** @microsoft/bf-adaptive -/tests/Microsoft.Bot.Builder.LanguageGeneration.Tests/** @microsoft/bf-adaptive -/libraries/AdaptiveExpressions/** @microsoft/bf-adaptive -/tests/AdaptiveExpressions.Tests/** @microsoft/bf-adaptive +/libraries/Microsoft.Bot.Builder.Dialogs.Declarative/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms +/tests/Microsoft.Bot.Builder.Dialogs.Declarative.Tests/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms +/libraries/Microsoft.Bot.Builder.LanguageGeneration/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms +/tests/Microsoft.Bot.Builder.LanguageGeneration.Tests/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms +/libraries/AdaptiveExpressions/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms +/tests/AdaptiveExpressions.Tests/** @microsoft/bf-adaptive @EricDahlvang @mrivera-ms # BotBuilder Dialogs Debugging /libraries/Microsoft.Bot.Builder.Dialogs.Debugging/** @mrivera-ms @tomlm @gabog @@ -74,29 +74,30 @@ /tests/Microsoft.Bot.Builder.TemplateManager/** @mrivera-ms @tomlm # BotBuilder Testing -/libraries/Microsoft.Bot.Builder.Testing/** @gabog -/tests/Microsoft.Bot.Builder.Testing.Tests/** @gabog +/libraries/Microsoft.Bot.Builder.Testing/** @gabog @EricDahlvang +/tests/Microsoft.Bot.Builder.Testing.Tests/** @gabog @EricDahlvang # Bot Framework Schema -/libraries/Microsoft.Bot.Schema/** @mrivera-ms @johnataylor -/tests/Microsoft.Bot.Schema.Tests/** @mrivera-ms @johnataylor +/libraries/Microsoft.Bot.Schema/** @microsoft/bb-core +/tests/Microsoft.Bot.Schema.Tests/** @microsoft/bb-core # Streaming library /libraries/Microsoft.Bot.Builder/Streaming/** @microsoft/bf-streaming /libraries/Microsoft.Bot.Streaming/** @microsoft/bf-streaming /tests/Microsoft.Bot.Builder.Streaming.Tests/** @microsoft/bf-streaming +/tests/Microsoft.Bot.Streaming.Tests/** @microsoft/bf-streaming # BotBuilder library -/libraries/Microsoft.Bot.Builder/** @gabog @johnataylor -/tests/Microsoft.Bot.Builder.Tests/** @gabog @johnataylor +/libraries/Microsoft.Bot.Builder/** @microsoft/bb-core +/tests/Microsoft.Bot.Builder.Tests/** @microsoft/bb-core # BotBuilder Dialogs /libraries/Microsoft.Bot.Builder.Dialogs/** @microsoft/bf-dialogs /tests/Microsoft.Bot.Builder.Dialogs/** @microsoft/bf-dialogs # Bot Framework Connector -/libraries/Microsoft.Bot.Connector/** @mrivera-ms @carlosscastro @johnataylor -/tests/Microsoft.Bot.Connector.Tests/** @mrivera-ms @carlosscastro @johnataylor +/libraries/Microsoft.Bot.Connector/** @microsoft/bb-core +/tests/Microsoft.Bot.Connector.Tests/** @microsoft/bb-core # Bot Framework Authentication /libraries/Microsoft.Bot.Builder/OAuth/** @microsoft/bf-auth diff --git a/libraries/Microsoft.Bot.Builder/IBotTelemetryClientExtensions.cs b/libraries/Microsoft.Bot.Builder/IBotTelemetryClientExtensions.cs index 151df344e0..753da40ee2 100644 --- a/libraries/Microsoft.Bot.Builder/IBotTelemetryClientExtensions.cs +++ b/libraries/Microsoft.Bot.Builder/IBotTelemetryClientExtensions.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; namespace Microsoft.Bot.Builder { diff --git a/libraries/Microsoft.Bot.Builder/IntentScore.cs b/libraries/Microsoft.Bot.Builder/IntentScore.cs index ad7cd15032..609fc9ca4b 100644 --- a/libraries/Microsoft.Bot.Builder/IntentScore.cs +++ b/libraries/Microsoft.Bot.Builder/IntentScore.cs @@ -1,5 +1,6 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. + using System.Collections.Generic; using Newtonsoft.Json; diff --git a/libraries/Microsoft.Bot.Builder/RegisterClassMiddleware.cs b/libraries/Microsoft.Bot.Builder/RegisterClassMiddleware.cs index b049c787b3..54ef0fcc21 100644 --- a/libraries/Microsoft.Bot.Builder/RegisterClassMiddleware.cs +++ b/libraries/Microsoft.Bot.Builder/RegisterClassMiddleware.cs @@ -1,4 +1,7 @@ -using System.Threading; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Bot.Builder diff --git a/libraries/Microsoft.Bot.Builder/Streaming/BotFrameworkHttpAdapterBase.cs b/libraries/Microsoft.Bot.Builder/Streaming/BotFrameworkHttpAdapterBase.cs index fb5f335eb5..919c1c426f 100644 --- a/libraries/Microsoft.Bot.Builder/Streaming/BotFrameworkHttpAdapterBase.cs +++ b/libraries/Microsoft.Bot.Builder/Streaming/BotFrameworkHttpAdapterBase.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -116,19 +119,31 @@ public async Task ProcessStreamingActivityAsync(Activity activit BotAssert.ActivityNotNull(activity); Logger.LogInformation($"Received an incoming streaming activity. ActivityId: {activity.Id}"); + + // If a StreamingRequestHandler.Audience is a null value, then no callerId should have been generated + // and GetAudienceFromCallerId returns null. + // Thus we fallback to relying on the "original key", essentially $"{ServiceUrl}{Conversation.Id}", + // as opposed to $"{ServiceUrl}{Audience}{Conversation.Id}" and the StreamingRequestHandler implicitly does not support skills. + var audience = GetAudienceFromCallerId(activity); // If a conversation has moved from one connection to another for the same Channel or Skill and // hasn't been forgotten by the previous StreamingRequestHandler. The last requestHandler // the conversation has been associated with should always be the active connection. - var requestHandler = RequestHandlers.Where(x => x.ServiceUrl == activity.ServiceUrl).Where(y => y.HasConversation(activity.Conversation.Id)).LastOrDefault(); + var requestHandler = RequestHandlers.Where( + h => h.ServiceUrl == activity.ServiceUrl + && h.Audience == audience + && h.HasConversation(activity.Conversation.Id)) + .LastOrDefault(); using (var context = new TurnContext(this, activity)) { + context.TurnState.Add(OAuthScopeKey, audience); + // Pipes are unauthenticated. Pending to check that we are in pipes right now. Do not merge to master without that. if (ClaimsIdentity != null) { context.TurnState.Add(BotIdentityKey, ClaimsIdentity); } - + var connectorClient = CreateStreamingConnectorClient(activity, requestHandler); context.TurnState.Add(connectorClient); @@ -217,9 +232,10 @@ public async Task SendStreamingActivityAsync(Activity activity /// /// The name of the Named Pipe to connect to. /// The bot to use when processing activities received over the Named Pipe. + /// The specified recipient of all outgoing activities. /// A task that completes only once the StreamingRequestHandler has stopped listening /// for incoming requests on the Named Pipe. - public async Task ConnectNamedPipeAsync(string pipeName, IBot bot) + public async Task ConnectNamedPipeAsync(string pipeName, IBot bot, string audience = null) { if (string.IsNullOrEmpty(pipeName)) { @@ -234,7 +250,7 @@ public async Task ConnectNamedPipeAsync(string pipeName, IBot bot) RequestHandlers = new List(); } - var requestHandler = new StreamingRequestHandler(bot, this, pipeName, Logger); + var requestHandler = new StreamingRequestHandler(bot, this, pipeName, audience, Logger); RequestHandlers.Add(requestHandler); await requestHandler.ListenAsync().ConfigureAwait(false); @@ -295,5 +311,27 @@ private IConnectorClient CreateStreamingConnectorClient(Activity activity, Strea var connectorClient = new ConnectorClient(new Uri(activity.ServiceUrl), emptyCredentials, customHttpClient: streamingClient); return connectorClient; } + + /// + /// Attempts to get an audience from the . + /// + /// The incoming activity to be processed by a . + private string GetAudienceFromCallerId(Activity activity) + { + switch (activity.CallerId) + { + case CallerIdConstants.PublicAzureChannel: + return AuthenticationConstants.ToChannelFromBotOAuthScope; + case CallerIdConstants.USGovChannel: + return GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope; + default: + if (activity.CallerId.StartsWith(CallerIdConstants.BotToBotPrefix, StringComparison.InvariantCultureIgnoreCase)) + { + return activity.CallerId.Substring(CallerIdConstants.BotToBotPrefix.Length); + } + + return null; + } + } } } diff --git a/libraries/Microsoft.Bot.Builder/Streaming/StreamingRequestHandler.cs b/libraries/Microsoft.Bot.Builder/Streaming/StreamingRequestHandler.cs index 74e7cdf5af..cdecfaad59 100644 --- a/libraries/Microsoft.Bot.Builder/Streaming/StreamingRequestHandler.cs +++ b/libraries/Microsoft.Bot.Builder/Streaming/StreamingRequestHandler.cs @@ -13,6 +13,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.Connector; +using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Schema; using Microsoft.Bot.Streaming; using Microsoft.Bot.Streaming.Transport; @@ -48,15 +49,38 @@ public class StreamingRequestHandler : RequestHandler /// The base socket to use when connecting to the channel. /// Logger implementation for tracing and debugging information. public StreamingRequestHandler(IBot bot, IStreamingActivityProcessor activityProcessor, WebSocket socket, ILogger logger = null) + : this(bot, activityProcessor, socket, null, logger) + { + } + + /// + /// Initializes a new instance of the class and + /// establishes a connection over a WebSocket to a streaming channel. + /// + /// + /// The audience represents the recipient at the other end of the StreamingRequestHandler's + /// streaming connection. Some acceptable audience values are as follows: + /// + /// - For Public Azure channels, use . + /// - For Azure Government channels, use . + /// + /// + /// The bot for which we handle requests. + /// The processor for incoming requests. + /// The base socket to use when connecting to the channel. + /// Logger implementation for tracing and debugging information. + /// The specified recipient of all outgoing activities. + public StreamingRequestHandler(IBot bot, IStreamingActivityProcessor activityProcessor, WebSocket socket, string audience, ILogger logger = null) { _bot = bot ?? throw new ArgumentNullException(nameof(bot)); _activityProcessor = activityProcessor ?? throw new ArgumentNullException(nameof(activityProcessor)); - + if (socket == null) { throw new ArgumentNullException(nameof(socket)); } + Audience = audience; _logger = logger ?? NullLogger.Instance; _conversations = new ConcurrentDictionary(); _userAgent = GetUserAgent(); @@ -74,6 +98,28 @@ public StreamingRequestHandler(IBot bot, IStreamingActivityProcessor activityPro /// The name of the Named Pipe to use when connecting to the channel. /// Logger implementation for tracing and debugging information. public StreamingRequestHandler(IBot bot, IStreamingActivityProcessor activityProcessor, string pipeName, ILogger logger = null) + : this(bot, activityProcessor, pipeName, null, logger) + { + } + + /// + /// Initializes a new instance of the class and + /// establishes a connection over a Named Pipe to a streaming channel. + /// + /// + /// The audience represents the recipient at the other end of the StreamingRequestHandler's + /// streaming connection. Some acceptable audience values are as follows: + /// + /// - For Public Azure channels, use . + /// - For Azure Government channels, use . + /// + /// + /// The bot for which we handle requests. + /// The processor for incoming requests. + /// The name of the Named Pipe to use when connecting to the channel. + /// Logger implementation for tracing and debugging information. + /// The specified recipient of all outgoing activities. + public StreamingRequestHandler(IBot bot, IStreamingActivityProcessor activityProcessor, string pipeName, string audience, ILogger logger = null) { _bot = bot ?? throw new ArgumentNullException(nameof(bot)); _activityProcessor = activityProcessor ?? throw new ArgumentNullException(nameof(activityProcessor)); @@ -84,6 +130,7 @@ public StreamingRequestHandler(IBot bot, IStreamingActivityProcessor activityPro throw new ArgumentNullException(nameof(pipeName)); } + Audience = audience; _conversations = new ConcurrentDictionary(); _userAgent = GetUserAgent(); _server = new NamedPipeServer(pipeName, this); @@ -101,6 +148,14 @@ public StreamingRequestHandler(IBot bot, IStreamingActivityProcessor activityPro public string ServiceUrl { get; private set; } #pragma warning restore CA1056 // Uri properties should not be strings + /// + /// Gets the intended recipient of Activities sent from this StreamingRequestHandler. + /// + /// + /// The intended recipient of Activities sent from this StreamingRequestHandler. + /// + public string Audience { get; private set; } + /// /// Begins listening for incoming requests over this StreamingRequestHandler's server. /// @@ -225,6 +280,43 @@ public override async Task ProcessRequestAsync(ReceiveRequest } } + // Populate Activity.CallerId given the Audience value. + string callerId = null; + switch (Audience) + { + case AuthenticationConstants.ToChannelFromBotOAuthScope: + callerId = CallerIdConstants.PublicAzureChannel; + break; + case GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope: + callerId = CallerIdConstants.USGovChannel; + break; + default: + if (!string.IsNullOrEmpty(Audience)) + { + if (Guid.TryParse(Audience, out var result)) + { + // Large assumption drawn here; any GUID is an AAD AppId. This is prohibitive towards bots not using the Bot Framework auth model + // but still using GUIDs/UUIDs as identifiers. + // It's also indicative of the tight coupling between the Bot Framework protocol, authentication and transport mechanism in the SDK. + // In R12, this work will be re-implemented to better utilize the CallerId and Audience set on BotFrameworkAuthentication instances + // and decouple the three concepts mentioned above. + callerId = $"{CallerIdConstants.BotToBotPrefix}{Audience}"; + } + else + { + // Fallback to using the raw Audience as the CallerId. The auth model being used by the Adapter using this StreamingRequestHandler + // is not known to the SDK, therefore it is assumed the developer knows what they're doing. The SDK should not prevent + // the developer from extending it to use custom auth models in Streaming contexts. + callerId = Audience; + } + } + + // A null Audience is an implicit statement indicating the bot does not support skills. + break; + } + + activity.CallerId = callerId; + // Now that the request has been converted into an activity we can send it to the adapter. var adapterResponse = await _activityProcessor.ProcessStreamingActivityAsync(activity, _bot.OnTurnAsync, cancellationToken).ConfigureAwait(false); diff --git a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ApplicationBuilderExtensions.cs b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ApplicationBuilderExtensions.cs index 2ed84079ff..4dc7a84947 100644 --- a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ApplicationBuilderExtensions.cs +++ b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ApplicationBuilderExtensions.cs @@ -77,8 +77,9 @@ public static IApplicationBuilder UseBotFramework(this IApplicationBuilder appli /// /// The application builder that defines the bot's pipeline.. /// The name of the named pipe to use when creating the server. + /// The specified recipient of all outgoing activities. /// A reference to this instance after the operation has completed. - public static IApplicationBuilder UseNamedPipes(this IApplicationBuilder applicationBuilder, string pipeName = "bfv4.pipes") + public static IApplicationBuilder UseNamedPipes(this IApplicationBuilder applicationBuilder, string pipeName = "bfv4.pipes", string audience = null) { if (applicationBuilder == null) { @@ -86,7 +87,7 @@ public static IApplicationBuilder UseNamedPipes(this IApplicationBuilder applica } var bot = applicationBuilder.ApplicationServices.GetService(typeof(IBot)) as IBot; - _ = (applicationBuilder.ApplicationServices.GetService(typeof(IBotFrameworkHttpAdapter)) as BotFrameworkHttpAdapter).ConnectNamedPipeAsync(pipeName, bot); + _ = (applicationBuilder.ApplicationServices.GetService(typeof(IBotFrameworkHttpAdapter)) as BotFrameworkHttpAdapter).ConnectNamedPipeAsync(pipeName, bot, audience); return applicationBuilder; } diff --git a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/BotFrameworkHttpAdapter.cs b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/BotFrameworkHttpAdapter.cs index e630aa7efb..2ca69d601d 100644 --- a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/BotFrameworkHttpAdapter.cs +++ b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/BotFrameworkHttpAdapter.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -218,7 +219,8 @@ private async Task ConnectWebSocketAsync(IBot bot, HttpRequest httpRequest, Http return; } - if (!await AuthenticateRequestAsync(httpRequest).ConfigureAwait(false)) + var claimsIdentity = await AuthenticateRequestAsync(httpRequest).ConfigureAwait(false); + if (claimsIdentity == null) { httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; await httpRequest.HttpContext.Response.WriteAsync("Request authentication failed.").ConfigureAwait(false); @@ -229,8 +231,11 @@ private async Task ConnectWebSocketAsync(IBot bot, HttpRequest httpRequest, Http try { var socket = await httpRequest.HttpContext.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); - - var requestHandler = new StreamingRequestHandler(bot, this, socket, Logger); + + // Set ClaimsIdentity on Adapter to enable Skills and User OAuth in WebSocket-based streaming scenarios. + var audience = GetAudience(claimsIdentity); + + var requestHandler = new StreamingRequestHandler(bot, this, socket, audience, Logger); if (RequestHandlers == null) { @@ -250,7 +255,14 @@ private async Task ConnectWebSocketAsync(IBot bot, HttpRequest httpRequest, Http } } - private async Task AuthenticateRequestAsync(HttpRequest httpRequest) + /// + /// Validates the auth header for WebSocket upgrade requests. + /// + /// + /// Returns a ClaimsIdentity for successful auth and when auth is disabled. Returns null for failed auth. + /// + /// The connection request. + private async Task AuthenticateRequestAsync(HttpRequest httpRequest) { try { @@ -262,28 +274,30 @@ private async Task AuthenticateRequestAsync(HttpRequest httpRequest) if (string.IsNullOrWhiteSpace(authHeader)) { await WriteUnauthorizedResponseAsync(AuthHeaderName, httpRequest).ConfigureAwait(false); - return false; + return null; } if (string.IsNullOrWhiteSpace(channelId)) { await WriteUnauthorizedResponseAsync(ChannelIdHeaderName, httpRequest).ConfigureAwait(false); - return false; + return null; } var claimsIdentity = await JwtTokenValidation.ValidateAuthHeader(authHeader, CredentialProvider, ChannelProvider, channelId).ConfigureAwait(false); if (!claimsIdentity.IsAuthenticated) { httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; - return false; + return null; } // Add ServiceURL to the cache of trusted sites in order to allow token refreshing. AppCredentials.TrustServiceUrl(claimsIdentity.FindFirst(AuthenticationConstants.ServiceUrlClaim).Value); ClaimsIdentity = claimsIdentity; + return claimsIdentity; } - return true; + // Authentication is not enabled, therefore return an anonymous ClaimsIdentity. + return new ClaimsIdentity(new List(), "anonymous"); } catch (Exception) { @@ -293,5 +307,32 @@ private async Task AuthenticateRequestAsync(HttpRequest httpRequest) throw; } } + + /// + /// Get the audience for the WebSocket connection from the authenticated ClaimsIdentity. + /// + /// + /// Setting the Audience on the StreamingRequestHandler enables the bot to call skills and correctly forward responses from the skill to the next recipient. + /// i.e. the participant at the other end of the WebSocket connection. + /// + /// ClaimsIdentity for authenticated caller. + private string GetAudience(ClaimsIdentity claimsIdentity) + { + if (claimsIdentity.AuthenticationType != AuthenticationConstants.AnonymousAuthType) + { + var audience = ChannelProvider != null && ChannelProvider.IsGovernment() ? + GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope : + AuthenticationConstants.ToChannelFromBotOAuthScope; + + if (SkillValidation.IsSkillClaim(claimsIdentity.Claims)) + { + audience = JwtTokenValidation.GetAppIdFromClaims(claimsIdentity.Claims); + } + + return audience; + } + + return null; + } } } diff --git a/tests/Microsoft.Bot.Streaming.Tests/StreamingRequestHandlerTests.cs b/tests/Microsoft.Bot.Streaming.Tests/StreamingRequestHandlerTests.cs index 25de4224be..4f777f0f3b 100644 --- a/tests/Microsoft.Bot.Streaming.Tests/StreamingRequestHandlerTests.cs +++ b/tests/Microsoft.Bot.Streaming.Tests/StreamingRequestHandlerTests.cs @@ -21,16 +21,32 @@ namespace Microsoft.Bot.Builder.Streaming.Tests { public class StreamingRequestHandlerTests { - [Fact] - public void CanBeConstructedWithANamedPipe() + public static IEnumerable GetSuccessfulConstructorTestData(int scenario) + { + var testData = new List + { + new object[] { Guid.NewGuid().ToString(), null }, + new object[] { Guid.NewGuid().ToString(), "audience" }, + new object[] { new FauxSock(), null }, + new object[] { new FauxSock(), "audience" } + }; + + return new List { testData[scenario] }; + } + + [Theory] + [MemberData(nameof(GetSuccessfulConstructorTestData), parameters: 0)] + [MemberData(nameof(GetSuccessfulConstructorTestData), parameters: 1)] + public void CanBeConstructedWithANamedPipe(string namedPipe, string audience) { // Arrange // Act - var handler = new StreamingRequestHandler(new MockBot(), new BotFrameworkHttpAdapter(), Guid.NewGuid().ToString()); + var handler = new StreamingRequestHandler(new MockBot(), new BotFrameworkHttpAdapter(), namedPipe, audience); // Assert Assert.NotNull(handler); + Assert.Equal(audience, handler.Audience); } [Fact] @@ -73,16 +89,19 @@ public void ThrowsIfPipeNameIsBlank() Assert.IsType(result); } - [Fact] - public void CanBeConstructedWithAWebSocket() + [Theory] + [MemberData(nameof(GetSuccessfulConstructorTestData), parameters: 2)] + [MemberData(nameof(GetSuccessfulConstructorTestData), parameters: 3)] + public void CanBeConstructedWithAWebSocket(FauxSock socket, string audience) { // Arrange // Act - var handler = new StreamingRequestHandler(new MockBot(), new BotFrameworkHttpAdapter(), new FauxSock()); + var handler = new StreamingRequestHandler(new MockBot(), new BotFrameworkHttpAdapter(), socket, audience); // Assert Assert.NotNull(handler); + Assert.Equal(audience, handler.Audience); } [Fact] @@ -284,31 +303,7 @@ public async void ItGetsUserAgentInfo() Assert.Matches(expectation, response.Streams[0].Content.ReadAsStringAsync().Result); } - private class MessageBot : IBot - { - public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) => await turnContext.SendActivityAsync(MessageFactory.Text("do.not.go.gentle.into.that.good.night")); - } - - private class FakeContentStream : IContentStream - { - public FakeContentStream(Guid id, string contentType, Stream stream) - { - Id = id; - ContentType = contentType; - Stream = stream; - Length = int.Parse(stream.Length.ToString()); - } - - public Guid Id { get; set; } - - public string ContentType { get; set; } - - public int? Length { get; set; } - - public Stream Stream { get; set; } - } - - private class FauxSock : WebSocket + public class FauxSock : WebSocket { public override WebSocketCloseStatus? CloseStatus => throw new NotImplementedException(); @@ -335,7 +330,6 @@ public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string s public override void Dispose() { - throw new NotImplementedException(); } public override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) @@ -348,5 +342,29 @@ public override Task SendAsync(ArraySegment buffer, WebSocketMessageType m throw new NotImplementedException(); } } + + private class MessageBot : IBot + { + public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) => await turnContext.SendActivityAsync(MessageFactory.Text("do.not.go.gentle.into.that.good.night")); + } + + private class FakeContentStream : IContentStream + { + public FakeContentStream(Guid id, string contentType, Stream stream) + { + Id = id; + ContentType = contentType; + Stream = stream; + Length = int.Parse(stream.Length.ToString()); + } + + public Guid Id { get; set; } + + public string ContentType { get; set; } + + public int? Length { get; set; } + + public Stream Stream { get; set; } + } } }