From 44ac39227d9ef02766b29017999ab877d1fa1880 Mon Sep 17 00:00:00 2001 From: Kees Verhaar Date: Mon, 7 Dec 2020 12:18:29 +0100 Subject: [PATCH 1/3] Return the auth challenge in the WWW-Authenticate header on authentication failure --- .../Middleware/AuthenticationMiddleware.cs | 16 ++ src/Ocelot/Middleware/HttpItemsExtensions.cs | 6 + .../Multiplexer/MultiplexingMiddleware.cs | 57 ++--- src/Ocelot/Responder/HttpContextResponder.cs | 6 + src/Ocelot/Responder/IHttpResponder.cs | 4 +- .../Middleware/ResponderMiddleware.cs | 15 +- .../Authentication/AuthenticationTests.cs | 64 +++++ test/Ocelot.AcceptanceTests/Steps.cs | 236 +++++++++--------- 8 files changed, 253 insertions(+), 151 deletions(-) diff --git a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs index 22fa1dca0..3209c3dca 100644 --- a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs +++ b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs @@ -3,6 +3,8 @@ using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Middleware; +using System.Runtime.Remoting.Contexts; +using System.Threading.Tasks; namespace Ocelot.Authentication.Middleware { @@ -36,6 +38,7 @@ public async Task Invoke(HttpContext httpContext) if (result.Principal?.Identity == null) { + await ChallengeAsync(httpContext, downstreamRoute); SetUnauthenticatedError(httpContext, path, null); return; } @@ -49,6 +52,7 @@ public async Task Invoke(HttpContext httpContext) return; } + await ChallengeAsync(httpContext, downstreamRoute); SetUnauthenticatedError(httpContext, path, httpContext.User.Identity.Name); } @@ -59,6 +63,18 @@ private void SetUnauthenticatedError(HttpContext httpContext, string path, strin httpContext.Items.SetError(error); } + private async Task ChallengeAsync(HttpContext context, DownstreamRoute route) + { + // Perform a challenge. This populates the WWW-Authenticate header on the response + await context.ChallengeAsync(route.AuthenticationOptions.AuthenticationProviderKey); + + // Since the response gets re-created down the pipeline, we store the challenge in the Items, so we can re-apply it when sending the response + if (context.Response.Headers.TryGetValue("WWW-Authenticate", out var authenticateHeader)) + { + context.Items.SetAuthChallenge(authenticateHeader); + } + } + private async Task AuthenticateAsync(HttpContext context, DownstreamRoute route) { var options = route.AuthenticationOptions; diff --git a/src/Ocelot/Middleware/HttpItemsExtensions.cs b/src/Ocelot/Middleware/HttpItemsExtensions.cs index df97e0a19..fcdbe232f 100644 --- a/src/Ocelot/Middleware/HttpItemsExtensions.cs +++ b/src/Ocelot/Middleware/HttpItemsExtensions.cs @@ -43,6 +43,12 @@ public static void SetError(this IDictionary input, Error error) input.Upsert("Errors", errors); } + public static void SetAuthChallenge(this IDictionary input, string challengeString) => + input.Upsert("AuthChallenge", challengeString); + + public static string AuthChallenge(this IDictionary input) => + input.Get("AuthChallenge"); + public static void SetIInternalConfiguration(this IDictionary input, IInternalConfiguration config) { input.Upsert("IInternalConfiguration", config); diff --git a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs index c148eb9b9..7af79b5c6 100644 --- a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs +++ b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs @@ -1,14 +1,14 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Newtonsoft.Json.Linq; -using Ocelot.Configuration; +using Ocelot.Configuration; using Ocelot.Configuration.File; -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Logging; -using Ocelot.Middleware; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Logging; +using Ocelot.Middleware; using System.Collections; using Route = Ocelot.Configuration.Route; - + namespace Ocelot.Multiplexer; public class MultiplexingMiddleware : OcelotMiddleware @@ -16,7 +16,7 @@ public class MultiplexingMiddleware : OcelotMiddleware private readonly RequestDelegate _next; private readonly IResponseAggregatorFactory _factory; private const string RequestIdString = "RequestId"; - + public MultiplexingMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IResponseAggregatorFactory factory) @@ -25,7 +25,7 @@ public MultiplexingMiddleware(RequestDelegate next, _factory = factory; _next = next; } - + public async Task Invoke(HttpContext httpContext) { var downstreamRouteHolder = httpContext.Items.DownstreamRouteHolder(); @@ -38,13 +38,13 @@ public async Task Invoke(HttpContext httpContext) await ProcessSingleRouteAsync(httpContext, downstreamRoutes[0]); return; } - + // Case 2: if no downstream routes if (downstreamRoutes.Count == 0) { return; } - + // Case 3: if multiple downstream routes var routeKeysConfigs = route.DownstreamRouteConfig; if (routeKeysConfigs == null || routeKeysConfigs.Count == 0) @@ -52,23 +52,23 @@ public async Task Invoke(HttpContext httpContext) await ProcessRoutesAsync(httpContext, route); return; } - + // Case 4: if multiple downstream routes with route keys var mainResponseContext = await ProcessMainRouteAsync(httpContext, downstreamRoutes[0]); if (mainResponseContext == null) { return; } - + var responsesContexts = await ProcessRoutesWithRouteKeysAsync(httpContext, downstreamRoutes, routeKeysConfigs, mainResponseContext); if (responsesContexts.Length == 0) { return; } - + await MapResponsesAsync(httpContext, route, mainResponseContext, responsesContexts); } - + /// /// Helper method to determine if only the first downstream route should be processed. /// It is the case if the request is a websocket request or if there is only one downstream route. @@ -78,7 +78,7 @@ public async Task Invoke(HttpContext httpContext) /// True if only the first downstream route should be processed. private static bool ShouldProcessSingleRoute(HttpContext context, ICollection routes) => context.WebSockets.IsWebSocketRequest || routes.Count == 1; - + /// /// Processing a single downstream route (no route keys). /// In that case, no need to make copies of the http context. @@ -89,9 +89,10 @@ private static bool ShouldProcessSingleRoute(HttpContext context, ICollection ro protected virtual Task ProcessSingleRouteAsync(HttpContext context, DownstreamRoute route) { context.Items.UpsertDownstreamRoute(route); + context.Items.SetAuthChallenge(/*finished*/context.Items.AuthChallenge()); return _next.Invoke(context); } - + /// /// Processing the downstream routes (no route keys). /// @@ -105,7 +106,7 @@ private async Task ProcessRoutesAsync(HttpContext context, Route route) var contexts = await Task.WhenAll(tasks); await MapAsync(context, route, new(contexts)); } - + /// /// When using route keys, the first route is the main route and the rest are additional routes. /// Since we need to break if the main route response is null, we must process the main route first. @@ -119,7 +120,7 @@ private async Task ProcessMainRouteAsync(HttpContext context, Downs await _next.Invoke(context); return context; } - + /// /// Processing the downstream routes with route keys except the main route that has already been processed. /// @@ -133,7 +134,7 @@ protected virtual async Task ProcessRoutesWithRouteKeysAsync(Http var processing = new List>(); var content = await mainResponse.Items.DownstreamResponse().Content.ReadAsStringAsync(); var jObject = JToken.Parse(content); - + foreach (var downstreamRoute in routes.Skip(1)) { var matchAdvancedAgg = routeKeysConfigs.FirstOrDefault(q => q.RouteKey == downstreamRoute.Key); @@ -142,13 +143,13 @@ protected virtual async Task ProcessRoutesWithRouteKeysAsync(Http processing.AddRange(ProcessRouteWithComplexAggregation(matchAdvancedAgg, jObject, context, downstreamRoute)); continue; } - + processing.Add(ProcessRouteAsync(context, downstreamRoute)); } - + return await Task.WhenAll(processing); } - + /// /// Mapping responses. /// @@ -158,7 +159,7 @@ private Task MapResponsesAsync(HttpContext context, Route route, HttpContext mai contexts.AddRange(responsesContexts); return MapAsync(context, route, contexts); } - + /// /// Processing a route with aggregation. /// @@ -173,7 +174,7 @@ private IEnumerable> ProcessRouteWithComplexAggregation(Aggreg tPnv.Add(new PlaceholderNameAndValue('{' + matchAdvancedAgg.Parameter + '}', value)); processing.Add(ProcessRouteAsync(httpContext, downstreamRoute, tPnv)); } - + return processing; } @@ -186,11 +187,11 @@ private async Task ProcessRouteAsync(HttpContext sourceContext, Dow var newHttpContext = await CreateThreadContextAsync(sourceContext, route); CopyItemsToNewContext(newHttpContext, sourceContext, placeholders); newHttpContext.Items.UpsertDownstreamRoute(route); - + await _next.Invoke(newHttpContext); return newHttpContext; } - + /// /// Copying some needed parameters to the Http context items. /// @@ -247,7 +248,7 @@ protected virtual async Task CreateThreadContextAsync(HttpContext s target.Response.RegisterForDisposeAsync(bodyStream); // manage Stream lifetime by HttpResponse object return target; } - + protected virtual Task MapAsync(HttpContext httpContext, Route route, List contexts) { if (route.DownstreamRoute.Count == 1) @@ -282,4 +283,4 @@ protected virtual async Task CloneRequestBodyAsync(HttpRequest request, return targetBuffer; } -} +} diff --git a/src/Ocelot/Responder/HttpContextResponder.cs b/src/Ocelot/Responder/HttpContextResponder.cs index 326c4d95a..3e15c4f1c 100644 --- a/src/Ocelot/Responder/HttpContextResponder.cs +++ b/src/Ocelot/Responder/HttpContextResponder.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Primitives; using Ocelot.Headers; using Ocelot.Middleware; +using System.Runtime.Remoting.Messaging; namespace Ocelot.Responder; @@ -77,6 +78,11 @@ public async Task SetErrorResponseOnContext(HttpContext context, DownstreamRespo } } + public void SetAuthChallengeOnContext(HttpContext context, string challenge) + { + AddHeaderIfDoesntExist(context, new Header("WWW-Authenticate", new[] { challenge })); + } + private static void SetStatusCode(HttpContext context, int statusCode) { if (!context.Response.HasStarted) diff --git a/src/Ocelot/Responder/IHttpResponder.cs b/src/Ocelot/Responder/IHttpResponder.cs index 588a863e3..f73a2c9f9 100644 --- a/src/Ocelot/Responder/IHttpResponder.cs +++ b/src/Ocelot/Responder/IHttpResponder.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Http; using Ocelot.Middleware; - + namespace Ocelot.Responder { public interface IHttpResponder @@ -10,5 +10,7 @@ public interface IHttpResponder void SetErrorResponseOnContext(HttpContext context, int statusCode); Task SetErrorResponseOnContext(HttpContext context, DownstreamResponse response); + + void SetAuthChallengeOnContext(HttpContext context, string challenge); } } diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs index a79a22c18..958e713f9 100644 --- a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs +++ b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs @@ -61,13 +61,20 @@ private async Task SetErrorResponse(HttpContext context, List errors) var statusCode = _codeMapper.Map(errors); _responder.SetErrorResponseOnContext(context, statusCode); - if (errors.All(e => e.Code != OcelotErrorCode.QuotaExceededError)) + if (errors.Any(e => e.Code == OcelotErrorCode.QuotaExceededError)) { - return; + var downstreamResponse = context.Items.DownstreamResponse(); + await _responder.SetErrorResponseOnContext(context, downstreamResponse); } - var downstreamResponse = context.Items.DownstreamResponse(); - await _responder.SetErrorResponseOnContext(context, downstreamResponse); + if (errors.Any(e => e.Code == OcelotErrorCode.UnauthenticatedError)) + { + var challenge = context.Items.AuthChallenge(); + if (!string.IsNullOrEmpty(challenge)) + { + _responder.SetAuthChallengeOnContext(context, challenge); + } + } } } } diff --git a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs index c66f8e194..9e06a076d 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs @@ -2,6 +2,8 @@ using IdentityServer4.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using System.Net.Http; namespace Ocelot.AcceptanceTests.Authentication { @@ -112,6 +114,68 @@ public void Should_return_201_using_identity_server_reference_token() .BDDfy(); } + [Fact] + [Trait("Feat", "1387")] + public void Should_return_www_authenticate_header_on_401() + { + var port = PortFinder.GetRandomPort(); + var route = GivenDefaultAuthRoute(port); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithJwtAuth("Test")) + .And(x => GivenIHaveNoTokenForMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) + .And(x => ThenTheResponseShouldContainAuthChallenge()) + .BDDfy(); + } + + public void GivenOcelotIsRunningWithJwtAuth(string authenticationProviderKey) + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + + var configuration = builder.Build(); + _webHostBuilder = new WebHostBuilder(); + _webHostBuilder.ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + }); + + _ocelotServer = new TestServer(_webHostBuilder + .UseConfiguration(configuration) + .ConfigureServices(s => + { + s.AddAuthentication().AddJwtBearer(authenticationProviderKey, options => + { + }); + s.AddOcelot(configuration); + }) + .ConfigureLogging(l => + { + l.AddConsole(); + l.AddDebug(); + }) + .Configure(a => + { + a.UseOcelot().Wait(); + })); + + _ocelotClient = _ocelotServer.CreateClient(); + } + public void GivenIHaveNoTokenForMyRequest() + { + _ocelotClient.DefaultRequestHeaders.Authorization = null; + } + public void ThenTheResponseShouldContainAuthChallenge() + { + _response.Headers.TryGetValues("WWW-Authenticate", out var headerValue).ShouldBeTrue(); + headerValue.ShouldNotBeEmpty(); + } + [IgnorePublicMethod] public async Task GivenThereIsAnIdentityServerOn(string url, AccessTokenType tokenType) { diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index ec1808906..da140e933 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -1,16 +1,16 @@ -using CacheManager.Core; -using IdentityServer4.AccessTokenValidation; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; +using CacheManager.Core; +using IdentityServer4.AccessTokenValidation; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using Ocelot.AcceptanceTests.Caching; -using Ocelot.Cache.CacheManager; +using Ocelot.Cache.CacheManager; using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; @@ -19,19 +19,19 @@ using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Provider.Eureka; -using Ocelot.Provider.Polly; -using Ocelot.Tracing.Butterfly; -using Ocelot.Tracing.OpenTracing; +using Ocelot.Provider.Polly; +using Ocelot.Tracing.Butterfly; +using Ocelot.Tracing.OpenTracing; using Serilog; using Serilog.Core; using System.IO.Compression; using System.Net.Http.Headers; using System.Text; -using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; -using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; -using CookieHeaderValue = Microsoft.Net.Http.Headers.CookieHeaderValue; -using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; - +using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; +using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; +using CookieHeaderValue = Microsoft.Net.Http.Headers.CookieHeaderValue; +using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; + namespace Ocelot.AcceptanceTests; public class Steps : IDisposable @@ -52,7 +52,7 @@ public class Steps : IDisposable private IWebHost _ocelotHost; // TODO remove because of one reference private IOcelotConfigurationChangeTokenSource _changeToken; - + public Steps() { _random = new Random(); @@ -61,7 +61,7 @@ public Steps() Files = new() { _ocelotConfigFileName }; Folders = new(); } - + protected List Files { get; } protected List Folders { get; } protected string TestID { get => _testId.ToString("N"); } @@ -88,13 +88,13 @@ public async Task ThenConfigShouldBe(FileConfiguration fileConfig) { var internalConfigCreator = _ocelotServer.Host.Services.GetService(); var internalConfigRepo = _ocelotServer.Host.Services.GetService(); - + var internalConfig = internalConfigRepo.Get(); var config = await internalConfigCreator.Create(fileConfig); - + internalConfig.Data.RequestId.ShouldBe(config.Data.RequestId); } - + public async Task ThenConfigShouldBeWithTimeout(FileConfiguration fileConfig, int timeoutMs) { var result = await Wait.WaitFor(timeoutMs).UntilAsync(async () => @@ -165,7 +165,7 @@ protected string SerializeJson(FileConfiguration from, ref string toFile) Files.Add(toFile); // register for disposing return JsonConvert.SerializeObject(from, Formatting.Indented); } - + protected virtual void DeleteFiles() { foreach (var file in Files) @@ -204,23 +204,23 @@ protected virtual void DeleteFolders() } } } - + public void ThenTheResponseBodyHeaderIs(string key, string value) { var header = _response.Content.Headers.GetValues(key); header.First().ShouldBe(value); } - + public void GivenOcelotIsRunningReloadingConfig(bool shouldReload) { StartOcelot((_, config) => config.AddJsonFile(_ocelotConfigFileName, false, shouldReload)); } - + public void GivenIHaveAChangeToken() { _changeToken = _ocelotServer.Host.Services.GetRequiredService(); } - + /// /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. /// @@ -228,7 +228,7 @@ public void GivenOcelotIsRunning() { StartOcelot((_, config) => config.AddJsonFile(_ocelotConfigFileName, false, false)); } - + protected void StartOcelot(Action configureAddOcelot, string environmentName = null) { _webHostBuilder = TestHostBuilder.Create() @@ -244,17 +244,17 @@ protected void StartOcelot(Action .ConfigureServices(WithAddOcelot) .Configure(WithUseOcelot) .UseEnvironment(environmentName ?? nameof(AcceptanceTests)); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public void ThenTheTraceHeaderIsSet(string key) { var header = _response.Headers.GetValues(key); header.First().ShouldNotBeNullOrEmpty(); } - + internal void GivenOcelotIsRunningUsingButterfly(string butterflyUrl) { _webHostBuilder = TestHostBuilder.Create() @@ -270,23 +270,23 @@ internal void GivenOcelotIsRunningUsingButterfly(string butterflyUrl) .ConfigureServices(s => { s.AddOcelot() - .AddButterfly(option => - { - //this is the url that the butterfly collector server is running on... - option.CollectorUrl = butterflyUrl; - option.Service = "Ocelot"; - }); + .AddButterfly(option => + { + //this is the url that the butterfly collector server is running on... + option.CollectorUrl = butterflyUrl; + option.Service = "Ocelot"; + }); }) .Configure(async app => { app.Use(async (_, next) => { await next.Invoke(); }); await app.UseOcelot(); }); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public async Task WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url) { var result = await Wait.WaitFor(2000).UntilAsync(async () => @@ -302,10 +302,10 @@ public async Task WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string u return false; } }); - + result.ShouldBeTrue(); } - + public void GivenOcelotIsRunningUsingJsonSerializedCache() { _webHostBuilder = TestHostBuilder.Create() @@ -332,13 +332,13 @@ public void GivenOcelotIsRunningUsingJsonSerializedCache() }); }) .Configure(async app => await app.UseOcelot()); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public static void GivenIWait(int wait) => Thread.Sleep(wait); - + public void GivenOcelotIsRunningWithMiddlewareBeforePipeline(Func callback) { _webHostBuilder = TestHostBuilder.Create() @@ -357,11 +357,11 @@ public void GivenOcelotIsRunningWithMiddlewareBeforePipeline(Func(callback); await app.UseOcelot(); }); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public void GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi() where TOne : DelegatingHandler where TWo : DelegatingHandler @@ -384,11 +384,11 @@ public void GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi() .AddDelegatingHandler(); }) .Configure(async a => await a.UseOcelot()); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() where TOne : DelegatingHandler where TWo : DelegatingHandler @@ -411,11 +411,11 @@ public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() .AddDelegatingHandler(true); }) .Configure(async a => await a.UseOcelot()); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public void GivenOcelotIsRunningWithHandlerRegisteredInDi(bool global = false) where TOne : DelegatingHandler { @@ -436,11 +436,11 @@ public void GivenOcelotIsRunningWithHandlerRegisteredInDi(bool global = fa .AddDelegatingHandler(global); }) .Configure(async a => await a.UseOcelot()); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(FakeDependency dependency) where TOne : DelegatingHandler { @@ -462,7 +462,7 @@ public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(FakeDepen .AddDelegatingHandler(true); }) .Configure(async a => await a.UseOcelot()); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } @@ -515,22 +515,22 @@ public void GivenOcelotIsRunning(Action opt .AddIdentityServerAuthentication(authenticationProviderKey, options); }) .Configure(async app => await app.UseOcelot()); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public void ThenTheResponseHeaderIs(string key, string value) { var header = _response.Headers.GetValues(key); header.First().ShouldBe(value); } - + public void ThenTheReasonPhraseIs(string expected) { _response.ReasonPhrase.ShouldBe(expected); } - + public void GivenOcelotIsRunningWithServices(Action configureServices) => GivenOcelotIsRunningWithServices(configureServices, null); @@ -567,11 +567,11 @@ public void GivenOcelotIsRunning(OcelotPipelineConfiguration ocelotPipelineConfi .AddJsonFile("appsettings.json", true, false) .AddJsonFile(_ocelotConfigFileName, false, false) .AddEnvironmentVariables(); - + var configuration = builder.Build(); _webHostBuilder = TestHostBuilder.Create() .ConfigureServices(s => { s.AddSingleton(_webHostBuilder); }); - + _ocelotServer = new TestServer(_webHostBuilder .UseConfiguration(configuration) .ConfigureServices(s => { s.AddOcelot(configuration); }) @@ -581,7 +581,7 @@ public void GivenOcelotIsRunning(OcelotPipelineConfiguration ocelotPipelineConfi l.AddDebug(); }) .Configure(async a => await a.UseOcelot(ocelotPipelineConfig))); - + _ocelotClient = _ocelotServer.CreateClient(); } @@ -599,19 +599,19 @@ public void GivenIHaveAddedATokenToMyRequest() new ("password", "test"), new ("grant_type", "password"), }; - + internal Task GivenIHaveAToken(string url) { var form = GivenDefaultAuthTokenForm(); return GivenIHaveATokenWithForm(url, form); } - + internal async Task GivenIHaveATokenWithForm(string url, IEnumerable> form) { var tokenUrl = $"{url}/connect/token"; var formData = form ?? Enumerable.Empty>(); var content = new FormUrlEncodedContent(formData); - + using var httpClient = new HttpClient(); var response = await httpClient.PostAsync(tokenUrl, content); var responseContent = await response.Content.ReadAsStringAsync(); @@ -619,7 +619,7 @@ internal async Task GivenIHaveATokenWithForm(string url, IEnumerabl _token = JsonConvert.DeserializeObject(responseContent); return _token; } - + public static async Task VerifyIdentityServerStarted(string url) { using var httpClient = new HttpClient(); @@ -627,13 +627,13 @@ public static async Task VerifyIdentityServerStarted(string url) await response.Content.ReadAsStringAsync(); response.EnsureSuccessStatusCode(); } - + public void GivenOcelotIsRunningWithMinimumLogLevel(Logger logger, string appsettingsFileName) { _webHostBuilder = TestHostBuilder.Create() .UseKestrel() .ConfigureAppConfiguration((_, config) => - { + { config.AddJsonFile(appsettingsFileName, false, false); config.AddJsonFile(_ocelotConfigFileName, false, false); config.AddEnvironmentVariables(); @@ -661,61 +661,61 @@ public void GivenOcelotIsRunningWithMinimumLogLevel(Logger logger, string appset new Exception("test")); await next.Invoke(); - }); + }); await app.UseOcelot(); }); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public void GivenOcelotIsRunningWithEureka() => GivenOcelotIsRunningWithServices(s => s.AddOcelot().AddEureka()); - + public void GivenOcelotIsRunningWithPolly() => GivenOcelotIsRunningWithServices(WithPolly); public static void WithPolly(IServiceCollection services) => services.AddOcelot().AddPolly(); - + public async Task WhenIGetUrlOnTheApiGateway(string url) => _response = await _ocelotClient.GetAsync(url); - + public Task WhenIGetUrl(string url) => _ocelotClient.GetAsync(url); - + public async Task WhenIGetUrlWithBodyOnTheApiGateway(string url, string body) { var request = new HttpRequestMessage(HttpMethod.Get, url) - { + { Content = new StringContent(body), }; _response = await _ocelotClient.SendAsync(request); } - + public async Task WhenIGetUrlWithFormOnTheApiGateway(string url, string name, IEnumerable> values) { var content = new MultipartFormDataContent(); var dataContent = new FormUrlEncodedContent(values); content.Add(dataContent, name); content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); - + var request = new HttpRequestMessage(HttpMethod.Get, url) - { + { Content = content, }; _response = await _ocelotClient.SendAsync(request); } - + public async Task WhenIGetUrlOnTheApiGateway(string url, HttpContent content) { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url) { Content = content }; _response = await _ocelotClient.SendAsync(httpRequestMessage); } - + public async Task WhenIPostUrlOnTheApiGateway(string url, HttpContent content) { var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url) { Content = content }; _response = await _ocelotClient.SendAsync(httpRequestMessage); } - + public void GivenIAddAHeader(string key, string value) { _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(key, value); @@ -732,7 +732,7 @@ public static async Task WhenIDoActionMultipleTimes(int times, Func a for (int i = 0; i < times; i++) await action.Invoke(i); } - + public async Task WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times) { for (var i = 0; i < times; i++) @@ -743,28 +743,28 @@ public async Task WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url _response = await _ocelotClient.SendAsync(request); } } - + public async Task WhenIGetUrlOnTheApiGateway(string url, string requestId) { _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); _response = await _ocelotClient.GetAsync(url); } - + public async Task WhenIPostUrlOnTheApiGateway(string url) { _response = await _ocelotClient.PostAsync(url, _postContent); } - + public void GivenThePostHasContent(string postContent) { _postContent = new StringContent(postContent); } - + public void GivenThePostHasContentType(string postContent) { _postContent.Headers.ContentType = new MediaTypeHeaderValue(postContent); } - + public void GivenThePostHasGzipContent(object input) { var json = JsonConvert.SerializeObject(input); @@ -774,33 +774,33 @@ public void GivenThePostHasGzipContent(object input) { gzip.Write(jsonBytes, 0, jsonBytes.Length); } - + ms.Position = 0; var content = new StreamContent(ms); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); content.Headers.ContentEncoding.Add("gzip"); _postContent = content; } - + public void ThenTheResponseBodyShouldBe(string expectedBody) => _response.Content.ReadAsStringAsync().GetAwaiter().GetResult().ShouldBe(expectedBody); public void ThenTheResponseBodyShouldBe(string expectedBody, string customMessage) => _response.Content.ReadAsStringAsync().GetAwaiter().GetResult().ShouldBe(expectedBody, customMessage); - + public void ThenTheContentLengthIs(int expected) => _response.Content.Headers.ContentLength.ShouldBe(expected); - + public void ThenTheStatusCodeShouldBe(HttpStatusCode expected) => _response.StatusCode.ShouldBe(expected); public void ThenTheStatusCodeShouldBe(int expected) => ((int)_response.StatusCode).ShouldBe(expected); - + public void ThenTheRequestIdIsReturned() => _response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty(); - + public void ThenTheRequestIdIsReturned(string expected) => _response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected); - + public void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() { var numberOfRequests = 100; @@ -811,33 +811,33 @@ public void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() var lauraUrl = "/laura"; var lauraExpected = "{Hello from Laura}"; var random = new Random(); - + var aggregateTasks = new Task[numberOfRequests]; - + for (var i = 0; i < numberOfRequests; i++) { aggregateTasks[i] = Fire(aggregateUrl, aggregateExpected, random); } - + var tomTasks = new Task[numberOfRequests]; - + for (var i = 0; i < numberOfRequests; i++) { tomTasks[i] = Fire(tomUrl, tomExpected, random); } - + var lauraTasks = new Task[numberOfRequests]; - + for (var i = 0; i < numberOfRequests; i++) { lauraTasks[i] = Fire(lauraUrl, lauraExpected, random); } - + Task.WaitAll(lauraTasks); Task.WaitAll(tomTasks); Task.WaitAll(aggregateTasks); } - + private async Task Fire(string url, string expectedBody, Random random) { var request = new HttpRequestMessage(new HttpMethod("GET"), url); @@ -846,7 +846,7 @@ private async Task Fire(string url, string expectedBody, Random random) var content = await response.Content.ReadAsStringAsync(); content.ShouldBe(expectedBody); } - + public void GivenOcelotIsRunningWithBlowingUpDiskRepo(IFileConfigurationRepository fake) { _webHostBuilder = TestHostBuilder.Create() @@ -865,16 +865,16 @@ public void GivenOcelotIsRunningWithBlowingUpDiskRepo(IFileConfigurationReposito s.AddOcelot(); }) .Configure(async app => await app.UseOcelot()); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public void TheChangeTokenShouldBeActive(bool itShouldBeActive) { _changeToken.ChangeToken.HasChanged.ShouldBe(itShouldBeActive); } - + public void GivenOcelotIsRunningWithLogger() { _webHostBuilder = TestHostBuilder.Create() @@ -893,11 +893,11 @@ public void GivenOcelotIsRunningWithLogger() s.AddSingleton(); }) .Configure(async app => await app.UseOcelot()); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + internal void GivenOcelotIsRunningUsingOpenTracing(OpenTracing.ITracer fakeTracer) { _webHostBuilder = TestHostBuilder.Create() @@ -914,7 +914,7 @@ internal void GivenOcelotIsRunningUsingOpenTracing(OpenTracing.ITracer fakeTrace { s.AddOcelot() .AddOpenTracing(); - + s.AddSingleton(fakeTracer); }) .Configure(async app => @@ -922,21 +922,21 @@ internal void GivenOcelotIsRunningUsingOpenTracing(OpenTracing.ITracer fakeTrace app.Use(async (_, next) => { await next.Invoke(); }); await app.UseOcelot(); }); - + _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public void ThenWarningShouldBeLogged(int howMany) { var loggerFactory = (MockLoggerFactory)_ocelotServer.Host.Services.GetService(); loggerFactory.Verify(Times.Exactly(howMany)); } - + internal class MockLoggerFactory : IOcelotLoggerFactory { private Mock _logger; - + public IOcelotLogger CreateLogger() { if (_logger != null) @@ -956,7 +956,7 @@ public void Verify(Times howMany) _logger.Verify(x => x.LogWarning(It.IsAny>()), howMany); } } - + /// /// Public implementation of Dispose pattern callable by consumers. /// @@ -965,7 +965,7 @@ public virtual void Dispose() Dispose(true); GC.SuppressFinalize(this); } - + private bool _disposedValue; /// @@ -987,8 +987,8 @@ protected virtual void Dispose(bool disposing) _response?.Dispose(); DeleteFiles(); DeleteFolders(); - } + } _disposedValue = true; - } -} + } +} From 96bc51c22b6269306d4f90857d4d85f42930509f Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Sat, 15 Jul 2023 16:55:02 +0300 Subject: [PATCH 2/3] Update Steps.cs --- test/Ocelot.AcceptanceTests/Steps.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index da140e933..44b43def9 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -533,7 +533,7 @@ public void ThenTheReasonPhraseIs(string expected) public void GivenOcelotIsRunningWithServices(Action configureServices) => GivenOcelotIsRunningWithServices(configureServices, null); - + public void GivenOcelotIsRunningWithServices(Action configureServices, Action configureApp/*, bool validateScopes*/) { _webHostBuilder = TestHostBuilder.Create() // ValidateScopes = true @@ -543,7 +543,7 @@ public void GivenOcelotIsRunningWithServices(Action configur _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); } - + public void WithBasicConfiguration(WebHostBuilderContext hosting, IConfigurationBuilder config) { var env = hosting.HostingEnvironment; @@ -553,7 +553,7 @@ public void WithBasicConfiguration(WebHostBuilderContext hosting, IConfiguration config.AddJsonFile(_ocelotConfigFileName, true, false); config.AddEnvironmentVariables(); } - + public static void WithAddOcelot(IServiceCollection services) => services.AddOcelot(); public static void WithUseOcelot(IApplicationBuilder app) => app.UseOcelot().Wait(); @@ -584,7 +584,7 @@ public void GivenOcelotIsRunning(OcelotPipelineConfiguration ocelotPipelineConfi _ocelotClient = _ocelotServer.CreateClient(); } - + public void GivenIHaveAddedATokenToMyRequest() { _ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); @@ -647,7 +647,7 @@ public void GivenOcelotIsRunningWithMinimumLogLevel(Logger logger, string appset .Configure(async app => { app.Use(async (context, next) => - { + { var loggerFactory = context.RequestServices.GetService(); var ocelotLogger = loggerFactory.CreateLogger(); ocelotLogger.LogDebug(() => $"DEBUG: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); From 34eb95521fe099a4b1315195ae1f869e3395248e Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Thu, 7 Nov 2024 12:36:22 +0300 Subject: [PATCH 3/3] Quickly review the code to fix the build, anticipating that there will be failed tests --- .../Middleware/AuthenticationMiddleware.cs | 10 ++-- src/Ocelot/Responder/HttpContextResponder.cs | 5 +- .../Authentication/AuthenticationTests.cs | 48 ++++--------------- 3 files changed, 15 insertions(+), 48 deletions(-) diff --git a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs index 3209c3dca..2b9e74d45 100644 --- a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs +++ b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs @@ -3,8 +3,6 @@ using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Middleware; -using System.Runtime.Remoting.Contexts; -using System.Threading.Tasks; namespace Ocelot.Authentication.Middleware { @@ -38,7 +36,7 @@ public async Task Invoke(HttpContext httpContext) if (result.Principal?.Identity == null) { - await ChallengeAsync(httpContext, downstreamRoute); + await ChallengeAsync(httpContext, downstreamRoute, result); SetUnauthenticatedError(httpContext, path, null); return; } @@ -52,7 +50,7 @@ public async Task Invoke(HttpContext httpContext) return; } - await ChallengeAsync(httpContext, downstreamRoute); + await ChallengeAsync(httpContext, downstreamRoute, result); SetUnauthenticatedError(httpContext, path, httpContext.User.Identity.Name); } @@ -63,10 +61,10 @@ private void SetUnauthenticatedError(HttpContext httpContext, string path, strin httpContext.Items.SetError(error); } - private async Task ChallengeAsync(HttpContext context, DownstreamRoute route) + private async Task ChallengeAsync(HttpContext context, DownstreamRoute route, AuthenticateResult status) { // Perform a challenge. This populates the WWW-Authenticate header on the response - await context.ChallengeAsync(route.AuthenticationOptions.AuthenticationProviderKey); + await context.ChallengeAsync(route.AuthenticationOptions.AuthenticationProviderKey); // TODO Read failed scheme from auth result // Since the response gets re-created down the pipeline, we store the challenge in the Items, so we can re-apply it when sending the response if (context.Response.Headers.TryGetValue("WWW-Authenticate", out var authenticateHeader)) diff --git a/src/Ocelot/Responder/HttpContextResponder.cs b/src/Ocelot/Responder/HttpContextResponder.cs index 3e15c4f1c..d9cc9e986 100644 --- a/src/Ocelot/Responder/HttpContextResponder.cs +++ b/src/Ocelot/Responder/HttpContextResponder.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Primitives; using Ocelot.Headers; using Ocelot.Middleware; -using System.Runtime.Remoting.Messaging; namespace Ocelot.Responder; @@ -79,9 +78,7 @@ public async Task SetErrorResponseOnContext(HttpContext context, DownstreamRespo } public void SetAuthChallengeOnContext(HttpContext context, string challenge) - { - AddHeaderIfDoesntExist(context, new Header("WWW-Authenticate", new[] { challenge })); - } + => AddHeaderIfDoesntExist(context, new Header("WWW-Authenticate", new[] { challenge })); private static void SetStatusCode(HttpContext context, int statusCode) { diff --git a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs index 9e06a076d..aa6262f31 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs @@ -2,8 +2,8 @@ using IdentityServer4.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.DependencyInjection; namespace Ocelot.AcceptanceTests.Authentication { @@ -129,48 +129,20 @@ public void Should_return_www_authenticate_header_on_401() .And(x => ThenTheResponseShouldContainAuthChallenge()) .BDDfy(); } - - public void GivenOcelotIsRunningWithJwtAuth(string authenticationProviderKey) + private void GivenOcelotIsRunningWithJwtAuth(string authenticationProviderKey) { - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile("ocelot.json", false, false) - .AddEnvironmentVariables(); - - var configuration = builder.Build(); - _webHostBuilder = new WebHostBuilder(); - _webHostBuilder.ConfigureServices(s => + GivenOcelotIsRunningWithServices(WithJwtBearer); + void WithJwtBearer(IServiceCollection s) { - s.AddSingleton(_webHostBuilder); - }); - - _ocelotServer = new TestServer(_webHostBuilder - .UseConfiguration(configuration) - .ConfigureServices(s => - { - s.AddAuthentication().AddJwtBearer(authenticationProviderKey, options => - { - }); - s.AddOcelot(configuration); - }) - .ConfigureLogging(l => - { - l.AddConsole(); - l.AddDebug(); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - })); - - _ocelotClient = _ocelotServer.CreateClient(); + s.AddAuthentication().AddJwtBearer(authenticationProviderKey, options => { }); + s.AddOcelot(); + } } - public void GivenIHaveNoTokenForMyRequest() + private void GivenIHaveNoTokenForMyRequest() { _ocelotClient.DefaultRequestHeaders.Authorization = null; } - public void ThenTheResponseShouldContainAuthChallenge() + private void ThenTheResponseShouldContainAuthChallenge() { _response.Headers.TryGetValues("WWW-Authenticate", out var headerValue).ShouldBeTrue(); headerValue.ShouldNotBeEmpty();