diff --git a/src/Ocelot.Library/Infrastructure/Authentication/CouldNotFindConfigurationError.cs b/src/Ocelot.Library/Infrastructure/Authentication/CouldNotFindConfigurationError.cs new file mode 100644 index 00000000..5b30c463 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Authentication/CouldNotFindConfigurationError.cs @@ -0,0 +1,11 @@ +namespace Ocelot.Library.Infrastructure.Authentication +{ + using Responses; + public class CouldNotFindConfigurationError : Error + { + public CouldNotFindConfigurationError(string message) + : base(message) + { + } + } +} diff --git a/src/Ocelot.Library/Infrastructure/Authentication/IRouteRequiresAuthentication.cs b/src/Ocelot.Library/Infrastructure/Authentication/IRouteRequiresAuthentication.cs new file mode 100644 index 00000000..0a3f9858 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Authentication/IRouteRequiresAuthentication.cs @@ -0,0 +1,10 @@ +namespace Ocelot.Library.Infrastructure.Authentication +{ + using DownstreamRouteFinder; + using Responses; + + public interface IRouteRequiresAuthentication + { + Response IsAuthenticated(DownstreamRoute downstreamRoute, string httpMethod); + } +} \ No newline at end of file diff --git a/src/Ocelot.Library/Infrastructure/Authentication/RouteRequiresAuthentication.cs b/src/Ocelot.Library/Infrastructure/Authentication/RouteRequiresAuthentication.cs new file mode 100644 index 00000000..988959e4 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Authentication/RouteRequiresAuthentication.cs @@ -0,0 +1,35 @@ +namespace Ocelot.Library.Infrastructure.Authentication +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Configuration; + using DownstreamRouteFinder; + using Responses; + + public class RouteRequiresAuthentication : IRouteRequiresAuthentication + { + private readonly IOcelotConfiguration _configuration; + + public RouteRequiresAuthentication(IOcelotConfiguration configuration) + { + _configuration = configuration; + } + + public Response IsAuthenticated(DownstreamRoute downstreamRoute, string httpMethod) + { + var reRoute = + _configuration.ReRoutes.FirstOrDefault( + x => + x.DownstreamTemplate == downstreamRoute.DownstreamUrlTemplate && + string.Equals(x.UpstreamHttpMethod, httpMethod, StringComparison.CurrentCultureIgnoreCase)); + + if (reRoute == null) + { + return new ErrorResponse(new List {new CouldNotFindConfigurationError($"Could not find configuration for {downstreamRoute.DownstreamUrlTemplate} using method {httpMethod}")}); + } + + return new OkResponse(reRoute.IsAuthenticated); + } + } +} diff --git a/src/Ocelot.Library/Infrastructure/Configuration/OcelotConfiguration.cs b/src/Ocelot.Library/Infrastructure/Configuration/OcelotConfiguration.cs index be544d6a..7ac93b3c 100644 --- a/src/Ocelot.Library/Infrastructure/Configuration/OcelotConfiguration.cs +++ b/src/Ocelot.Library/Infrastructure/Configuration/OcelotConfiguration.cs @@ -43,7 +43,9 @@ namespace Ocelot.Library.Infrastructure.Configuration upstreamTemplate = $"{upstreamTemplate}{RegExMatchEndString}"; - _reRoutes.Add(new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, reRoute.UpstreamHttpMethod, upstreamTemplate)); + var isAuthenticated = !string.IsNullOrEmpty(reRoute.Authentication); + + _reRoutes.Add(new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated)); } } diff --git a/src/Ocelot.Library/Infrastructure/Configuration/ReRoute.cs b/src/Ocelot.Library/Infrastructure/Configuration/ReRoute.cs index 6d0f6497..9c5edf0f 100644 --- a/src/Ocelot.Library/Infrastructure/Configuration/ReRoute.cs +++ b/src/Ocelot.Library/Infrastructure/Configuration/ReRoute.cs @@ -2,17 +2,19 @@ { public class ReRoute { - public ReRoute(string downstreamTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern) + public ReRoute(string downstreamTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern, bool isAuthenticated) { DownstreamTemplate = downstreamTemplate; UpstreamTemplate = upstreamTemplate; UpstreamHttpMethod = upstreamHttpMethod; UpstreamTemplatePattern = upstreamTemplatePattern; + IsAuthenticated = isAuthenticated; } public string DownstreamTemplate { get; private set; } public string UpstreamTemplate { get; private set; } public string UpstreamTemplatePattern { get; private set; } public string UpstreamHttpMethod { get; private set; } + public bool IsAuthenticated { get; private set; } } } \ No newline at end of file diff --git a/src/Ocelot.Library/Infrastructure/Configuration/Yaml/YamlReRoute.cs b/src/Ocelot.Library/Infrastructure/Configuration/Yaml/YamlReRoute.cs index b012d19a..0c1e1e12 100644 --- a/src/Ocelot.Library/Infrastructure/Configuration/Yaml/YamlReRoute.cs +++ b/src/Ocelot.Library/Infrastructure/Configuration/Yaml/YamlReRoute.cs @@ -5,5 +5,6 @@ public string DownstreamTemplate { get; set; } public string UpstreamTemplate { get; set; } public string UpstreamHttpMethod { get; set; } + public string Authentication { get; set; } } } \ No newline at end of file diff --git a/src/Ocelot.Library/Middleware/AuthenticationMiddleware.cs b/src/Ocelot.Library/Middleware/AuthenticationMiddleware.cs new file mode 100644 index 00000000..21f680eb --- /dev/null +++ b/src/Ocelot.Library/Middleware/AuthenticationMiddleware.cs @@ -0,0 +1,54 @@ +namespace Ocelot.Library.Middleware +{ + using System.Threading.Tasks; + using Infrastructure.Authentication; + using Infrastructure.DownstreamRouteFinder; + using Infrastructure.Repository; + using Infrastructure.Responses; + using Microsoft.AspNetCore.Http; + + public class AuthenticationMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IScopedRequestDataRepository _scopedRequestDataRepository; + private readonly IRouteRequiresAuthentication _requiresAuthentication; + public AuthenticationMiddleware(RequestDelegate next, + IScopedRequestDataRepository scopedRequestDataRepository, + IRouteRequiresAuthentication requiresAuthentication) + : base(scopedRequestDataRepository) + { + _next = next; + _scopedRequestDataRepository = scopedRequestDataRepository; + _requiresAuthentication = requiresAuthentication; + } + + public async Task Invoke(HttpContext context) + { + var downstreamRoute = _scopedRequestDataRepository.Get("DownstreamRoute"); + + var isAuthenticated = _requiresAuthentication.IsAuthenticated(downstreamRoute.Data, context.Request.Method); + + if (isAuthenticated.IsError) + { + SetPipelineError(downstreamRoute.Errors); + return; + } + + if (IsAuthenticatedRoute(isAuthenticated)) + { + //todo - build auth pipeline and then call normal pipeline if all good? + await _next.Invoke(context); + } + else + { + await _next.Invoke(context); + } + + } + + private static bool IsAuthenticatedRoute(Response isAuthenticated) + { + return isAuthenticated.Data; + } + } +} diff --git a/src/Ocelot.Library/Middleware/AuthenticationMiddlewareMiddlewareExtensions.cs b/src/Ocelot.Library/Middleware/AuthenticationMiddlewareMiddlewareExtensions.cs new file mode 100644 index 00000000..3655e470 --- /dev/null +++ b/src/Ocelot.Library/Middleware/AuthenticationMiddlewareMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Library.Middleware +{ + public static class AuthenticationMiddlewareMiddlewareExtensions + { + public static IApplicationBuilder UseAuthenticationMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot.Library/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot.Library/Middleware/DownstreamUrlCreatorMiddleware.cs index 2627a722..00d5d76b 100644 --- a/src/Ocelot.Library/Middleware/DownstreamUrlCreatorMiddleware.cs +++ b/src/Ocelot.Library/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Ocelot.Library.Infrastructure.DownstreamRouteFinder; using Ocelot.Library.Infrastructure.Repository; -using Ocelot.Library.Infrastructure.Responder; using Ocelot.Library.Infrastructure.UrlTemplateReplacer; namespace Ocelot.Library.Middleware diff --git a/src/Ocelot.Library/Middleware/DownstreamUrlCreatorMiddlewareExtensions.cs b/src/Ocelot.Library/Middleware/DownstreamUrlCreatorMiddlewareExtensions.cs index 0ba3e58c..325a7454 100644 --- a/src/Ocelot.Library/Middleware/DownstreamUrlCreatorMiddlewareExtensions.cs +++ b/src/Ocelot.Library/Middleware/DownstreamUrlCreatorMiddlewareExtensions.cs @@ -4,7 +4,7 @@ namespace Ocelot.Library.Middleware { public static class DownstreamUrlCreatorMiddlewareExtensions { - public static IApplicationBuilder UserDownstreamUrlCreatorMiddleware(this IApplicationBuilder builder) + public static IApplicationBuilder UseDownstreamUrlCreatorMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware(); } diff --git a/src/Ocelot/Startup.cs b/src/Ocelot/Startup.cs index e2ecb505..9056daf9 100644 --- a/src/Ocelot/Startup.cs +++ b/src/Ocelot/Startup.cs @@ -20,6 +20,8 @@ using Ocelot.Library.Middleware; namespace Ocelot { + using Library.Infrastructure.Authentication; + public class Startup { public Startup(IHostingEnvironment env) @@ -52,6 +54,7 @@ namespace Ocelot services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc services.AddSingleton(); @@ -69,7 +72,9 @@ namespace Ocelot app.UseDownstreamRouteFinderMiddleware(); - app.UserDownstreamUrlCreatorMiddleware(); + app.UseAuthenticationMiddleware(); + + app.UseDownstreamUrlCreatorMiddleware(); app.UseHttpRequestBuilderMiddleware(); diff --git a/test/Ocelot.AcceptanceTests/configuration.yaml b/test/Ocelot.AcceptanceTests/configuration.yaml index 7a2db711..42e7ab07 100644 --- a/test/Ocelot.AcceptanceTests/configuration.yaml +++ b/test/Ocelot.AcceptanceTests/configuration.yaml @@ -2,3 +2,4 @@ ReRoutes: - DownstreamTemplate: http://localhost:51879/ UpstreamTemplate: / UpstreamHttpMethod: Get + Authentication: IdentityServer diff --git a/test/Ocelot.UnitTests/Authentication/RequiresAuthenticationTests.cs b/test/Ocelot.UnitTests/Authentication/RequiresAuthenticationTests.cs new file mode 100644 index 00000000..5747c963 --- /dev/null +++ b/test/Ocelot.UnitTests/Authentication/RequiresAuthenticationTests.cs @@ -0,0 +1,96 @@ +namespace Ocelot.UnitTests.Authentication +{ + using System.Collections.Generic; + using Library.Infrastructure.Authentication; + using Library.Infrastructure.Configuration; + using Library.Infrastructure.DownstreamRouteFinder; + using Library.Infrastructure.Responses; + using Library.Infrastructure.UrlMatcher; + using Moq; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + + public class RequiresAuthenticationTests + { + private readonly RouteRequiresAuthentication _routeRequiresAuthentication; + private string _url; + private readonly Mock _config; + private Response _result; + private string _httpMethod; + + public RequiresAuthenticationTests() + { + _config = new Mock(); + _routeRequiresAuthentication = new RouteRequiresAuthentication(_config.Object); + } + + [Fact] + public void should_return_true_if_route_requires_authentication() + { + this.Given(x => x.GivenIHaveADownstreamUrl("http://www.bbc.co.uk")) + .And( + x => + x.GivenTheConfigurationForTheRouteIs(new ReRoute("http://www.bbc.co.uk", "/api/poo", "get", + "/api/poo$", true))) + .When(x => x.WhenICheckToSeeIfTheRouteShouldBeAuthenticated()) + .Then(x => x.ThenTheResultIs(true)) + .BDDfy(); + } + + [Fact] + public void should_return_false_if_route_requires_authentication() + { + this.Given(x => x.GivenIHaveADownstreamUrl("http://www.bbc.co.uk")) + .And( + x => + x.GivenTheConfigurationForTheRouteIs(new ReRoute("http://www.bbc.co.uk", "/api/poo", "get", + "/api/poo$", false))) + .When(x => x.WhenICheckToSeeIfTheRouteShouldBeAuthenticated()) + .Then(x => x.ThenTheResultIs(false)) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_no_matching_config() + { + this.Given(x => x.GivenIHaveADownstreamUrl("http://www.bbc.co.uk")) + .And(x => x.GivenTheConfigurationForTheRouteIs(new ReRoute(string.Empty, string.Empty, string.Empty, string.Empty,false))) + .When(x => x.WhenICheckToSeeIfTheRouteShouldBeAuthenticated()) + .Then(x => x.ThenAnErrorIsReturned()) + .BDDfy(); + } + + private void ThenAnErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + } + + public void GivenIHaveADownstreamUrl(string url) + { + _url = url; + } + + private void GivenTheConfigurationForTheRouteIs(ReRoute reRoute) + { + _httpMethod = reRoute.UpstreamHttpMethod; + + _config + .Setup(x => x.ReRoutes) + .Returns(new List + { + reRoute + }); + } + + private void WhenICheckToSeeIfTheRouteShouldBeAuthenticated() + { + _result = _routeRequiresAuthentication.IsAuthenticated(new DownstreamRoute(new List(), _url), _httpMethod); + } + + private void ThenTheResultIs(bool expected) + { + _result.Data.ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/OcelotConfigurationTests.cs b/test/Ocelot.UnitTests/Configuration/OcelotConfigurationTests.cs index 1c030e6f..56ee89b8 100644 --- a/test/Ocelot.UnitTests/Configuration/OcelotConfigurationTests.cs +++ b/test/Ocelot.UnitTests/Configuration/OcelotConfigurationTests.cs @@ -38,7 +38,7 @@ namespace Ocelot.UnitTests.Configuration .When(x => x.WhenIInstanciateTheOcelotConfig()) .Then(x => x.ThenTheReRoutesAre(new List { - new ReRoute("/products/{productId}","/api/products/{productId}", "Get", "/api/products/.*$") + new ReRoute("/products/{productId}","/api/products/{productId}", "Get", "/api/products/.*$", false) })) .BDDfy(); } @@ -61,7 +61,7 @@ namespace Ocelot.UnitTests.Configuration .When(x => x.WhenIInstanciateTheOcelotConfig()) .Then(x => x.ThenTheReRoutesAre(new List { - new ReRoute("/products/{productId}","/api/products/{productId}/variants/{variantId}", "Get", "/api/products/.*/variants/.*$") + new ReRoute("/products/{productId}","/api/products/{productId}/variants/{variantId}", "Get", "/api/products/.*/variants/.*$", false) })) .BDDfy(); } @@ -84,7 +84,7 @@ namespace Ocelot.UnitTests.Configuration .When(x => x.WhenIInstanciateTheOcelotConfig()) .Then(x => x.ThenTheReRoutesAre(new List { - new ReRoute("/products/{productId}","/api/products/{productId}/variants/{variantId}/", "Get", "/api/products/.*/variants/.*/$") + new ReRoute("/products/{productId}","/api/products/{productId}/variants/{variantId}/", "Get", "/api/products/.*/variants/.*/$", false) })) .BDDfy(); } @@ -107,7 +107,7 @@ namespace Ocelot.UnitTests.Configuration .When(x => x.WhenIInstanciateTheOcelotConfig()) .Then(x => x.ThenTheReRoutesAre(new List { - new ReRoute("/api/products/","/", "Get", "/$") + new ReRoute("/api/products/","/", "Get", "/$", false) })) .BDDfy(); } diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index ee35836b..3675459a 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -38,7 +38,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) .And(x => x.GivenTheConfigurationIs(new List { - new ReRoute("someDownstreamPath","someUpstreamPath", "Get", "someUpstreamPath") + new ReRoute("someDownstreamPath","someUpstreamPath", "Get", "someUpstreamPath", false) } )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) @@ -57,8 +57,8 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) .And(x => x.GivenTheConfigurationIs(new List { - new ReRoute("someDownstreamPath", "someUpstreamPath", "Get", string.Empty), - new ReRoute("someDownstreamPathForAPost", "someUpstreamPath", "Post", string.Empty) + new ReRoute("someDownstreamPath", "someUpstreamPath", "Get", string.Empty, false), + new ReRoute("someDownstreamPathForAPost", "someUpstreamPath", "Post", string.Empty, false) } )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) @@ -75,7 +75,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder this.Given(x => x.GivenThereIsAnUpstreamUrlPath("somePath")) .And(x => x.GivenTheConfigurationIs(new List { - new ReRoute("somPath", "somePath", "Get", "somePath") + new ReRoute("somPath", "somePath", "Get", "somePath", false) } )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(false))))