• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-dotnet / 388829

07 May 2024 09:08PM UTC coverage: 78.175% (+0.01%) from 78.165%
388829

push

CI-PR build

web-flow
Added messages for all UnauthorizedAccessException (#6786)

* Added messages for all UnauthorizedAccessException

* Adjust unit tests for changes in UnauthorizedAccessException messages

---------

Co-authored-by: Tracy Boehrer <trboehre@microsoft.com>

26191 of 33503 relevant lines covered (78.18%)

0.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.93
/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/BotFrameworkHttpAdapter.cs
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
// Licensed under the MIT License.
3

4
using System;
5
using System.Collections.Generic;
6
using System.Linq;
7
using System.Net;
8
using System.Net.Http;
9
using System.Net.WebSockets;
10
using System.Security.Claims;
11
using System.Threading;
12
using System.Threading.Tasks;
13
using Microsoft.AspNetCore.Http;
14
using Microsoft.Bot.Builder.BotFramework;
15
using Microsoft.Bot.Builder.Streaming;
16
using Microsoft.Bot.Connector.Authentication;
17
using Microsoft.Bot.Schema;
18
using Microsoft.Extensions.Configuration;
19
using Microsoft.Extensions.Logging;
20
using Microsoft.Rest.TransientFaultHandling;
21

22
namespace Microsoft.Bot.Builder.Integration.AspNet.Core
23
{
24
    /// <summary>
25
    /// A Bot Builder Adapter implementation used to handled bot Framework HTTP requests.
26
    /// </summary>
27
    /// <remarks>
28
    /// BotFrameworkAdapter is still supported but the recommended adapter is `CloudAdapter`.
29
    /// </remarks>
30
    public class BotFrameworkHttpAdapter : BotFrameworkHttpAdapterBase, IBotFrameworkHttpAdapter
31
    {
32
        private const string AuthHeaderName = "authorization";
33
        private const string ChannelIdHeaderName = "channelid";
34

35
        /// <summary>
36
        /// Initializes a new instance of the <see cref="BotFrameworkHttpAdapter"/> class,
37
        /// using a credential provider.
38
        /// </summary>
39
        /// <param name="credentialProvider">The credential provider.</param>
40
        /// <param name="authConfig">The authentication configuration.</param>
41
        /// <param name="channelProvider">The channel provider.</param>
42
        /// <param name="connectorClientRetryPolicy">Retry policy for retrying HTTP operations.</param>
43
        /// <param name="customHttpClient">The HTTP client.</param>
44
        /// <param name="middleware">The middleware to initially add to the adapter.</param>
45
        /// <param name="logger">The ILogger implementation this adapter should use.</param>
46
        /// <exception cref="ArgumentNullException">
47
        /// <paramref name="credentialProvider"/> is <c>null</c>.</exception>
48
        /// <remarks>Use a <see cref="MiddlewareSet"/> object to add multiple middleware
49
        /// components in the constructor. Use the IMiddleware method to
50
        /// add additional middleware to the adapter after construction.
51
        /// </remarks>
52
        public BotFrameworkHttpAdapter(
53
            ICredentialProvider credentialProvider,
54
            AuthenticationConfiguration authConfig,
55
            IChannelProvider channelProvider = null,
56
            RetryPolicy connectorClientRetryPolicy = null,
57
            HttpClient customHttpClient = null,
58
            IMiddleware middleware = null,
59
            ILogger logger = null)
60
            : base(credentialProvider, authConfig ?? new AuthenticationConfiguration(), channelProvider, connectorClientRetryPolicy, customHttpClient, middleware, logger)
×
61
        {
62
        }
1✔
63

64
        /// <summary>
65
        /// Initializes a new instance of the <see cref="BotFrameworkHttpAdapter"/> class,
66
        /// using a credential provider.
67
        /// </summary>
68
        /// <param name="credentialProvider">The credential provider.</param>
69
        /// <param name="channelProvider">The channel provider.</param>
70
        /// <param name="logger">The ILogger implementation this adapter should use.</param>
71
        public BotFrameworkHttpAdapter(ICredentialProvider credentialProvider = null, IChannelProvider channelProvider = null, ILogger<BotFrameworkHttpAdapter> logger = null)
72
            : this(credentialProvider ?? new SimpleCredentialProvider(), new AuthenticationConfiguration(), channelProvider, null, null, null, logger)
1✔
73
        {
74
        }
1✔
75

76
        /// <summary>
77
        /// Initializes a new instance of the <see cref="BotFrameworkHttpAdapter"/> class,
78
        /// using a credential provider.
79
        /// </summary>
80
        /// <param name="credentialProvider">The credential provider.</param>
81
        /// <param name="channelProvider">The channel provider.</param>
82
        /// <param name="httpClient">The <see cref="HttpClient"/> used.</param>
83
        /// <param name="logger">The ILogger implementation this adapter should use.</param>
84
        public BotFrameworkHttpAdapter(ICredentialProvider credentialProvider, IChannelProvider channelProvider, HttpClient httpClient, ILogger<BotFrameworkHttpAdapter> logger)
85
            : this(credentialProvider ?? new SimpleCredentialProvider(), new AuthenticationConfiguration(), channelProvider, null, httpClient, null, logger)
1✔
86
        {
87
        }
1✔
88

89
        /// <summary>
90
        /// Initializes a new instance of the <see cref="BotFrameworkHttpAdapter"/> class.
91
        /// </summary>
92
        /// <param name="configuration">An <see cref="IConfiguration"/> instance.</param>
93
        /// <param name="credentialProvider">The credential provider.</param>
94
        /// <param name="authConfig">The authentication configuration.</param>
95
        /// <param name="channelProvider">The channel provider.</param>
96
        /// <param name="connectorClientRetryPolicy">Retry policy for retrying HTTP operations.</param>
97
        /// <param name="customHttpClient">The HTTP client.</param>
98
        /// <param name="middleware">The middleware to initially add to the adapter.</param>
99
        /// <param name="logger">The ILogger implementation this adapter should use.</param>
100
        protected BotFrameworkHttpAdapter(
101
            IConfiguration configuration,
102
            ICredentialProvider credentialProvider,
103
            AuthenticationConfiguration authConfig = null,
104
            IChannelProvider channelProvider = null,
105
            RetryPolicy connectorClientRetryPolicy = null,
106
            HttpClient customHttpClient = null,
107
            IMiddleware middleware = null,
108
            ILogger logger = null)
109
            : this(credentialProvider ?? new ConfigurationCredentialProvider(configuration), authConfig ?? new AuthenticationConfiguration(), channelProvider ?? new ConfigurationChannelProvider(configuration), connectorClientRetryPolicy, customHttpClient, middleware, logger)
×
110
        {
111
            var openIdEndpoint = configuration.GetSection(AuthenticationConstants.BotOpenIdMetadataKey)?.Value;
×
112

113
            if (!string.IsNullOrEmpty(openIdEndpoint))
1✔
114
            {
115
                // Indicate which Cloud we are using, for example, Public or Sovereign.
116
                ChannelValidation.OpenIdMetadataUrl = openIdEndpoint;
1✔
117
                GovernmentChannelValidation.OpenIdMetadataUrl = openIdEndpoint;
1✔
118
            }
119
        }
1✔
120

121
        /// <summary>
122
        /// Initializes a new instance of the <see cref="BotFrameworkHttpAdapter"/> class.
123
        /// </summary>
124
        /// <param name="configuration">An <see cref="IConfiguration"/> instance.</param>
125
        /// <param name="logger">The ILogger implementation this adapter should use.</param>
126
        protected BotFrameworkHttpAdapter(IConfiguration configuration, ILogger<BotFrameworkHttpAdapter> logger = null)
127
            : this(configuration, new ConfigurationCredentialProvider(configuration), new AuthenticationConfiguration(), new ConfigurationChannelProvider(configuration), logger: logger)
1✔
128
        {
129
        }
1✔
130

131
        /// <summary>
132
        /// This method can be called from inside a POST method on any Controller implementation.
133
        /// </summary>
134
        /// <param name="httpRequest">The HTTP request object, typically in a POST handler by a Controller.</param>
135
        /// <param name="httpResponse">The HTTP response object.</param>
136
        /// <param name="bot">The bot implementation.</param>
137
        /// <param name="cancellationToken">A cancellation token that can be used by other objects
138
        /// or threads to receive notice of cancellation.</param>
139
        /// <returns>A task that represents the work queued to execute.</returns>
140
        public async Task ProcessAsync(HttpRequest httpRequest, HttpResponse httpResponse, IBot bot, CancellationToken cancellationToken = default)
141
        {
142
            if (httpRequest == null)
1✔
143
            {
144
                throw new ArgumentNullException(nameof(httpRequest));
×
145
            }
146

147
            if (httpResponse == null)
1✔
148
            {
149
                throw new ArgumentNullException(nameof(httpResponse));
×
150
            }
151

152
            if (bot == null)
1✔
153
            {
154
                throw new ArgumentNullException(nameof(bot));
×
155
            }
156

157
            if (httpRequest.Method == HttpMethods.Get)
1✔
158
            {
159
                await ConnectWebSocketAsync(bot, httpRequest, httpResponse, cancellationToken).ConfigureAwait(false);
1✔
160
            }
161
            else
162
            {
163
                // Deserialize the incoming Activity
164
                var activity = await HttpHelper.ReadRequestAsync<Activity>(httpRequest).ConfigureAwait(false);
1✔
165

166
                if (string.IsNullOrEmpty(activity?.Type))
1✔
167
                {
168
                    httpResponse.StatusCode = (int)HttpStatusCode.BadRequest;
1✔
169
                    Logger.LogWarning("BadRequest: Missing activity or activity type.");
1✔
170
                    return;
1✔
171
                }
172

173
                // Grab the auth header from the inbound http request
174
                var authHeader = httpRequest.Headers["Authorization"];
1✔
175

176
                try
177
                {
178
                    // Process the inbound activity with the bot
179
                    var invokeResponse = await ProcessActivityAsync(authHeader, activity, bot.OnTurnAsync, cancellationToken).ConfigureAwait(false);
1✔
180

181
                    // write the response, potentially serializing the InvokeResponse
182
                    await HttpHelper.WriteResponseAsync(httpResponse, invokeResponse).ConfigureAwait(false);
1✔
183
                }
1✔
184
                catch (UnauthorizedAccessException ex)
×
185
                {
186
                    // handle unauthorized here as this layer creates the http response
187
                    httpResponse.StatusCode = (int)HttpStatusCode.Unauthorized;
×
188

189
                    Logger.LogError(ex.ToString());
×
190
                }
×
191
            }
192
        }
1✔
193

194
        /// <summary>
195
        /// Create the <see cref="StreamingRequestHandler"/> for processing for a new Web Socket connection request.
196
        /// </summary>
197
        /// <param name="bot">The <see cref="IBot"/> implementation which will process the request.</param>
198
        /// <param name="socket">The <see cref="WebSocket"/> which the request will be received on.</param>
199
        /// <param name="audience">The authorized audience of the incoming connection request.</param>
200
        /// <returns>Returns a new <see cref="StreamingRequestHandler"/> implementation.</returns>
201
        public virtual StreamingRequestHandler CreateStreamingRequestHandler(IBot bot, WebSocket socket, string audience)
202
        {
203
            if (bot == null)
1✔
204
            {
205
                throw new ArgumentNullException(nameof(bot));
×
206
            }
207

208
            if (socket == null)
1✔
209
            {
210
                throw new ArgumentNullException(nameof(socket));
×
211
            }
212

213
            return new StreamingRequestHandler(bot, this, socket, audience, Logger);
1✔
214
        }
215

216
        private static async Task WriteUnauthorizedResponseAsync(string headerName, HttpRequest httpRequest)
217
        {
218
            httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
×
219
            await httpRequest.HttpContext.Response.WriteAsync($"Unable to authenticate. Missing header: {headerName}").ConfigureAwait(false);
×
220
        }
×
221

222
        /// <summary>
223
        /// Process the initial request to establish a long lived connection via a streaming server.
224
        /// </summary>
225
        /// <param name="bot">The <see cref="IBot"/> instance.</param>
226
        /// <param name="httpRequest">The connection request.</param>
227
        /// <param name="httpResponse">The response sent on error or connection termination.</param>
228
        /// <param name="cancellationToken">The cancellation token.</param>
229
        /// <returns>Returns on task completion.</returns>
230
        private async Task ConnectWebSocketAsync(IBot bot, HttpRequest httpRequest, HttpResponse httpResponse, CancellationToken cancellationToken = default)
231
        {
232
            if (httpRequest == null)
1✔
233
            {
234
                throw new ArgumentNullException(nameof(httpRequest));
×
235
            }
236

237
            if (httpResponse == null)
1✔
238
            {
239
                throw new ArgumentNullException(nameof(httpResponse));
×
240
            }
241

242
            ConnectedBot = bot ?? throw new ArgumentNullException(nameof(bot));
×
243

244
            if (!httpRequest.HttpContext.WebSockets.IsWebSocketRequest)
1✔
245
            {
246
                httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
×
247
                await httpRequest.HttpContext.Response.WriteAsync("Upgrade to WebSocket is required.").ConfigureAwait(false);
×
248

249
                return;
×
250
            }
251

252
            var claimsIdentity = await AuthenticateRequestAsync(httpRequest).ConfigureAwait(false);
1✔
253
            if (claimsIdentity == null)
1✔
254
            {
255
                httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
×
256
                await httpRequest.HttpContext.Response.WriteAsync("Request authentication failed.").ConfigureAwait(false);
×
257

258
                return;
×
259
            }
260

261
            try
262
            {
263
                // Set ClaimsIdentity on Adapter to enable Skills and User OAuth in WebSocket-based streaming scenarios.
264
                var audience = GetAudience(claimsIdentity);
1✔
265

266
                var socket = await httpRequest.HttpContext.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
1✔
267
                var requestHandler = CreateStreamingRequestHandler(bot, socket, audience);
1✔
268

269
                if (RequestHandlers == null)
1✔
270
                {
271
                    RequestHandlers = new List<StreamingRequestHandler>();
×
272
                }
273

274
                RequestHandlers.Add(requestHandler);
1✔
275

276
                Log.WebSocketConnectionStarted(Logger);
1✔
277
                await requestHandler.ListenAsync(cancellationToken).ConfigureAwait(false);
1✔
278
                Log.WebSocketConnectionCompleted(Logger);
1✔
279
            }
1✔
280
            catch (Exception ex)
×
281
            {
282
                httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
×
283
                await httpRequest.HttpContext.Response.WriteAsync($"Unable to create transport server. Error: {ex.ToString()}").ConfigureAwait(false);
×
284

285
                throw;
×
286
            }
287
        }
1✔
288

289
        /// <summary>
290
        /// Validates the auth header for WebSocket upgrade requests.
291
        /// </summary>
292
        /// <remarks>
293
        /// Returns a ClaimsIdentity for successful auth and when auth is disabled. Returns null for failed auth.
294
        /// </remarks>
295
        /// <param name="httpRequest">The connection request.</param>
296
        private async Task<ClaimsIdentity> AuthenticateRequestAsync(HttpRequest httpRequest)
297
        {
298
            try
299
            {
300
                if (!await CredentialProvider.IsAuthenticationDisabledAsync().ConfigureAwait(false))
1✔
301
                {
302
                    var authHeader = httpRequest.Headers.First(x => string.Equals(x.Key, AuthHeaderName, StringComparison.OrdinalIgnoreCase)).Value.FirstOrDefault();
×
303
                    var channelId = httpRequest.Headers.First(x => string.Equals(x.Key, ChannelIdHeaderName, StringComparison.OrdinalIgnoreCase)).Value.FirstOrDefault();
×
304

305
                    if (string.IsNullOrWhiteSpace(authHeader))
×
306
                    {
307
                        await WriteUnauthorizedResponseAsync(AuthHeaderName, httpRequest).ConfigureAwait(false);
×
308
                        return null;
×
309
                    }
310

311
                    if (string.IsNullOrWhiteSpace(channelId))
×
312
                    {
313
                        await WriteUnauthorizedResponseAsync(ChannelIdHeaderName, httpRequest).ConfigureAwait(false);
×
314
                        return null;
×
315
                    }
316

317
                    var claimsIdentity = await JwtTokenValidation.ValidateAuthHeader(authHeader, CredentialProvider, ChannelProvider, channelId).ConfigureAwait(false);
×
318
                    if (!claimsIdentity.IsAuthenticated)
×
319
                    {
320
                        httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
×
321
                        return null;
×
322
                    }
323
                    
324
                    ClaimsIdentity = claimsIdentity;
×
325
                    return claimsIdentity;
×
326
                }
327

328
                // Authentication is not enabled, therefore return an anonymous ClaimsIdentity.
329
                return new ClaimsIdentity(new List<Claim>(), "anonymous");
1✔
330
            }
331
            catch (Exception)
×
332
            {
333
                httpRequest.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
×
334
                await httpRequest.HttpContext.Response.WriteAsync("Error while attempting to authorize connection.").ConfigureAwait(false);
×
335

336
                throw;
×
337
            }
338
        }
1✔
339

340
        /// <summary>
341
        /// Get the audience for the WebSocket connection from the authenticated ClaimsIdentity.
342
        /// </summary>
343
        /// <remarks>
344
        /// Setting the Audience on the StreamingRequestHandler enables the bot to call skills and correctly forward responses from the skill to the next recipient.
345
        /// i.e. the participant at the other end of the WebSocket connection.
346
        /// </remarks>
347
        /// <param name="claimsIdentity">ClaimsIdentity for authenticated caller.</param>
348
        private string GetAudience(ClaimsIdentity claimsIdentity)
349
        {
350
            if (claimsIdentity.AuthenticationType != AuthenticationConstants.AnonymousAuthType)
1✔
351
            {
352
                var audience = ChannelProvider != null && ChannelProvider.IsGovernment() ?
×
353
    GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope :
×
354
    AuthenticationConstants.ToChannelFromBotOAuthScope;
×
355

356
                if (SkillValidation.IsSkillClaim(claimsIdentity.Claims))
×
357
                {
358
                    audience = JwtTokenValidation.GetAppIdFromClaims(claimsIdentity.Claims);
×
359
                }
360

361
                return audience;
×
362
            }
363

364
            return null;
1✔
365
        }
366

367
        private class Log
368
        {
369
            private static readonly Action<ILogger, Exception> _webSocketConnectionStarted =
1✔
370
                LoggerMessage.Define(LogLevel.Information, new EventId(1, nameof(WebSocketConnectionStarted)), "WebSocket connection started.");
1✔
371

372
            private static readonly Action<ILogger, Exception> _webSocketConnectionCompleted =
1✔
373
                LoggerMessage.Define(LogLevel.Information, new EventId(2, nameof(WebSocketConnectionCompleted)), "WebSocket connection completed.");
1✔
374

375
            public static void WebSocketConnectionStarted(ILogger logger) => _webSocketConnectionStarted(logger, null);
1✔
376

377
            public static void WebSocketConnectionCompleted(ILogger logger) => _webSocketConnectionCompleted(logger, null);
1✔
378
        }
379
    }
380
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc