From 320b442526eba88dac3684a48fcfcce2b8f52d90 Mon Sep 17 00:00:00 2001 From: TomPallister Date: Sat, 15 Oct 2016 13:50:43 +0100 Subject: [PATCH] got identity server access token acceptance test working, created factory for choosing auth handlers, a creator for making the auth handlers, some general refactoring...next step is injecting the config for the auth handler creator in some way or just passing it in --- .../Authentication/AuthenticationHandler.cs | 16 ++ .../AuthenticationHandlerCreator.cs | 32 ++++ .../AuthenticationProviderFactory.cs | 32 ++++ .../IAuthenticationHandlerCreator.cs | 11 ++ .../IAuthenticationProviderFactory.cs | 10 ++ ...nableToCreateAuthenticationHandlerError.cs | 12 ++ .../Infrastructure/Errors/OcelotErrorCode.cs | 3 +- .../Middleware/AuthenticationMiddleware.cs | 38 ++--- .../Middleware/HttpResponderMiddleware.cs | 1 - .../Responder/HttpContextResponder.cs | 6 +- src/Ocelot.Library/project.json | 55 +++---- src/Ocelot/Startup.cs | 6 +- .../AuthenticationTests.cs | 147 +++++++++++++----- .../Ocelot.AcceptanceTests/configuration.yaml | 2 +- .../AuthenticationProviderFactoryTests.cs | 88 +++++++++++ .../AuthenticationMiddlewareTests.cs | 5 +- 16 files changed, 369 insertions(+), 95 deletions(-) create mode 100644 src/Ocelot.Library/Infrastructure/Authentication/AuthenticationHandler.cs create mode 100644 src/Ocelot.Library/Infrastructure/Authentication/AuthenticationHandlerCreator.cs create mode 100644 src/Ocelot.Library/Infrastructure/Authentication/AuthenticationProviderFactory.cs create mode 100644 src/Ocelot.Library/Infrastructure/Authentication/IAuthenticationHandlerCreator.cs create mode 100644 src/Ocelot.Library/Infrastructure/Authentication/IAuthenticationProviderFactory.cs create mode 100644 src/Ocelot.Library/Infrastructure/Authentication/UnableToCreateAuthenticationHandlerError.cs create mode 100644 test/Ocelot.UnitTests/Authentication/AuthenticationProviderFactoryTests.cs diff --git a/src/Ocelot.Library/Infrastructure/Authentication/AuthenticationHandler.cs b/src/Ocelot.Library/Infrastructure/Authentication/AuthenticationHandler.cs new file mode 100644 index 00000000..3830e33a --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Authentication/AuthenticationHandler.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Http; + +namespace Ocelot.Library.Infrastructure.Authentication +{ + public class AuthenticationHandler + { + public AuthenticationHandler(string provider, RequestDelegate handler) + { + Provider = provider; + Handler = handler; + } + + public string Provider { get; private set; } + public RequestDelegate Handler { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot.Library/Infrastructure/Authentication/AuthenticationHandlerCreator.cs b/src/Ocelot.Library/Infrastructure/Authentication/AuthenticationHandlerCreator.cs new file mode 100644 index 00000000..1428f239 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Authentication/AuthenticationHandlerCreator.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Ocelot.Library.Infrastructure.Responses; + +namespace Ocelot.Library.Infrastructure.Authentication +{ + /// + /// Cannot unit test things in this class due to use of extension methods + /// + public class AuthenticationHandlerCreator : IAuthenticationHandlerCreator + { + public Response CreateIdentityServerAuthenticationHandler(IApplicationBuilder app) + { + var builder = app.New(); + + builder.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions + { + //todo sort these options out + Authority = "http://localhost:51888", + ScopeName = "api", + + RequireHttpsMetadata = false + }); + + builder.UseMvc(); + + var authenticationNext = builder.Build(); + + return new OkResponse(authenticationNext); + } + } +} \ No newline at end of file diff --git a/src/Ocelot.Library/Infrastructure/Authentication/AuthenticationProviderFactory.cs b/src/Ocelot.Library/Infrastructure/Authentication/AuthenticationProviderFactory.cs new file mode 100644 index 00000000..770462ca --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Authentication/AuthenticationProviderFactory.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Ocelot.Library.Infrastructure.Errors; +using Ocelot.Library.Infrastructure.Responses; + +namespace Ocelot.Library.Infrastructure.Authentication +{ + public class AuthenticationProviderFactory : IAuthenticationProviderFactory + { + private readonly IAuthenticationHandlerCreator _creator; + + public AuthenticationProviderFactory(IAuthenticationHandlerCreator creator) + { + _creator = creator; + } + + public Response Get(string provider, IApplicationBuilder app) + { + var handler = _creator.CreateIdentityServerAuthenticationHandler(app); + + if (!handler.IsError) + { + return new OkResponse(new AuthenticationHandler(provider, handler.Data)); + } + + return new ErrorResponse(new List + { + new UnableToCreateAuthenticationHandlerError($"Unable to create authentication handler for {provider}") + }); + } + } +} \ No newline at end of file diff --git a/src/Ocelot.Library/Infrastructure/Authentication/IAuthenticationHandlerCreator.cs b/src/Ocelot.Library/Infrastructure/Authentication/IAuthenticationHandlerCreator.cs new file mode 100644 index 00000000..cb1b2a91 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Authentication/IAuthenticationHandlerCreator.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Ocelot.Library.Infrastructure.Responses; + +namespace Ocelot.Library.Infrastructure.Authentication +{ + public interface IAuthenticationHandlerCreator + { + Response CreateIdentityServerAuthenticationHandler(IApplicationBuilder app); + } +} diff --git a/src/Ocelot.Library/Infrastructure/Authentication/IAuthenticationProviderFactory.cs b/src/Ocelot.Library/Infrastructure/Authentication/IAuthenticationProviderFactory.cs new file mode 100644 index 00000000..e9a25a54 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Authentication/IAuthenticationProviderFactory.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Builder; +using Ocelot.Library.Infrastructure.Responses; + +namespace Ocelot.Library.Infrastructure.Authentication +{ + public interface IAuthenticationProviderFactory + { + Response Get(string provider, IApplicationBuilder app); + } +} diff --git a/src/Ocelot.Library/Infrastructure/Authentication/UnableToCreateAuthenticationHandlerError.cs b/src/Ocelot.Library/Infrastructure/Authentication/UnableToCreateAuthenticationHandlerError.cs new file mode 100644 index 00000000..5637ef73 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/Authentication/UnableToCreateAuthenticationHandlerError.cs @@ -0,0 +1,12 @@ +using Ocelot.Library.Infrastructure.Errors; + +namespace Ocelot.Library.Infrastructure.Authentication +{ + public class UnableToCreateAuthenticationHandlerError : Error + { + public UnableToCreateAuthenticationHandlerError(string message) + : base(message, OcelotErrorCode.UnableToCreateAuthenticationHandlerError) + { + } + } +} diff --git a/src/Ocelot.Library/Infrastructure/Errors/OcelotErrorCode.cs b/src/Ocelot.Library/Infrastructure/Errors/OcelotErrorCode.cs index e746de55..4f7a1fd7 100644 --- a/src/Ocelot.Library/Infrastructure/Errors/OcelotErrorCode.cs +++ b/src/Ocelot.Library/Infrastructure/Errors/OcelotErrorCode.cs @@ -8,6 +8,7 @@ UnableToFindDownstreamRouteError, CannotAddDataError, CannotFindDataError, - UnableToCompleteRequestError + UnableToCompleteRequestError, + UnableToCreateAuthenticationHandlerError } } diff --git a/src/Ocelot.Library/Infrastructure/Middleware/AuthenticationMiddleware.cs b/src/Ocelot.Library/Infrastructure/Middleware/AuthenticationMiddleware.cs index 4a72d412..be8f54be 100644 --- a/src/Ocelot.Library/Infrastructure/Middleware/AuthenticationMiddleware.cs +++ b/src/Ocelot.Library/Infrastructure/Middleware/AuthenticationMiddleware.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -7,6 +8,11 @@ using Ocelot.Library.Infrastructure.DownstreamRouteFinder; using Ocelot.Library.Infrastructure.Errors; using Ocelot.Library.Infrastructure.Repository; using Ocelot.Library.Infrastructure.Responses; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Ocelot.Library.Infrastructure.Authentication; namespace Ocelot.Library.Infrastructure.Middleware { @@ -16,13 +22,15 @@ namespace Ocelot.Library.Infrastructure.Middleware private RequestDelegate _authenticationNext; private readonly IScopedRequestDataRepository _scopedRequestDataRepository; private readonly IApplicationBuilder _app; + private readonly IAuthenticationProviderFactory _authProviderFactory; public AuthenticationMiddleware(RequestDelegate next, IApplicationBuilder app, - IScopedRequestDataRepository scopedRequestDataRepository) + IScopedRequestDataRepository scopedRequestDataRepository, IAuthenticationProviderFactory authProviderFactory) : base(scopedRequestDataRepository) { _next = next; _scopedRequestDataRepository = scopedRequestDataRepository; + _authProviderFactory = authProviderFactory; _app = app; } @@ -38,25 +46,17 @@ namespace Ocelot.Library.Infrastructure.Middleware if (IsAuthenticatedRoute(downstreamRoute.Data.ReRoute)) { - //todo - build auth pipeline and then call normal pipeline if all good? - //create new app builder - var builder = _app.New(); - //set up any options for the authentication - var jwtBearerOptions = new JwtBearerOptions + var authenticationNext = _authProviderFactory.Get(downstreamRoute.Data.ReRoute.AuthenticationProvider, _app); + + if (!authenticationNext.IsError) { - AutomaticAuthenticate = true, - AutomaticChallenge = true, - RequireHttpsMetadata = false, - }; - //set the authentication middleware - builder.UseJwtBearerAuthentication(jwtBearerOptions); - //use mvc so we hit the catch all authorised controller - builder.UseMvc(); - //then build it - _authenticationNext = builder.Build(); - //then call it - await _authenticationNext(context); - //check if the user is authenticated + await authenticationNext.Data.Handler.Invoke(context); + } + else + { + SetPipelineError(authenticationNext.Errors); + } + if (context.User.Identity.IsAuthenticated) { await _next.Invoke(context); diff --git a/src/Ocelot.Library/Infrastructure/Middleware/HttpResponderMiddleware.cs b/src/Ocelot.Library/Infrastructure/Middleware/HttpResponderMiddleware.cs index 3f268a62..0c8fba3c 100644 --- a/src/Ocelot.Library/Infrastructure/Middleware/HttpResponderMiddleware.cs +++ b/src/Ocelot.Library/Infrastructure/Middleware/HttpResponderMiddleware.cs @@ -43,7 +43,6 @@ namespace Ocelot.Library.Infrastructure.Middleware { await _responder.CreateErrorResponse(context, 500); } - } else { diff --git a/src/Ocelot.Library/Infrastructure/Responder/HttpContextResponder.cs b/src/Ocelot.Library/Infrastructure/Responder/HttpContextResponder.cs index e1cfa8a1..e5e31fe4 100644 --- a/src/Ocelot.Library/Infrastructure/Responder/HttpContextResponder.cs +++ b/src/Ocelot.Library/Infrastructure/Responder/HttpContextResponder.cs @@ -3,7 +3,11 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Ocelot.Library.Infrastructure.Responder -{ +{ + /// + /// Cannot unit test things in this class due to methods not being implemented + /// on .net concretes used for testing + /// public class HttpContextResponder : IHttpResponder { public async Task CreateResponse(HttpContext context, HttpResponseMessage response) diff --git a/src/Ocelot.Library/project.json b/src/Ocelot.Library/project.json index 24790da9..f57d8566 100644 --- a/src/Ocelot.Library/project.json +++ b/src/Ocelot.Library/project.json @@ -1,34 +1,35 @@ { "version": "1.0.0-*", - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0", - "type": "platform" + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0", + "type": "platform" + }, + "Microsoft.AspNetCore.Mvc": "1.0.0", + "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0", + "Microsoft.Extensions.Configuration.Json": "1.0.0", + "Microsoft.Extensions.Logging": "1.0.0", + "Microsoft.Extensions.Logging.Console": "1.0.0", + "Microsoft.Extensions.Logging.Debug": "1.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", + "Microsoft.AspNetCore.Http": "1.0.0", + "System.Text.RegularExpressions": "4.1.0", + "YamlDotNet": "3.9.0", + "Microsoft.AspNetCore.Authentication.OAuth": "1.0.0", + "Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0", + "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0", + "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0", + "Microsoft.AspNetCore.Authentication.Google": "1.0.0", + "Microsoft.AspNetCore.Authentication.Facebook": "1.0.0", + "Microsoft.AspNetCore.Authentication.Twitter": "1.0.0", + "Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.0.0", + "Microsoft.AspNetCore.Authentication": "1.0.0", + "IdentityServer4.AccessTokenValidation": "1.0.1-rc2" }, - "Microsoft.AspNetCore.Mvc": "1.0.0", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", - "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0", - "Microsoft.Extensions.Configuration.Json": "1.0.0", - "Microsoft.Extensions.Logging": "1.0.0", - "Microsoft.Extensions.Logging.Console": "1.0.0", - "Microsoft.Extensions.Logging.Debug": "1.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", - "Microsoft.AspNetCore.Http": "1.0.0", - "System.Text.RegularExpressions": "4.1.0", - "YamlDotNet": "3.9.0", - "Microsoft.AspNetCore.Authentication.OAuth": "1.0.0", - "Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0", - "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0", - "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0", - "Microsoft.AspNetCore.Authentication.Google": "1.0.0", - "Microsoft.AspNetCore.Authentication.Facebook": "1.0.0", - "Microsoft.AspNetCore.Authentication.Twitter": "1.0.0", - "Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.0.0", - "Microsoft.AspNetCore.Authentication": "1.0.0" - }, "frameworks": { "netcoreapp1.4": { diff --git a/src/Ocelot/Startup.cs b/src/Ocelot/Startup.cs index 5c9e44e6..35934de3 100644 --- a/src/Ocelot/Startup.cs +++ b/src/Ocelot/Startup.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Ocelot.Library.Infrastructure.Authentication; using Ocelot.Library.Infrastructure.Configuration; using Ocelot.Library.Infrastructure.Configuration.Yaml; using Ocelot.Library.Infrastructure.DownstreamRouteFinder; @@ -41,7 +42,8 @@ namespace Ocelot { services.AddOptions(); services.AddMvc(); - + services.AddMvcCore().AddAuthorization().AddJsonFormatters(); + services.Configure(Configuration); services.AddAuthentication(); @@ -55,6 +57,8 @@ namespace Ocelot services.AddSingleton(); 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(); diff --git a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs index 6b7ca01c..bfefdc7f 100644 --- a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Text.Encodings.Web; using IdentityServer4.Models; using IdentityServer4.Services.InMemory; @@ -11,6 +12,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using Ocelot.Library.Infrastructure.Configuration.Yaml; using Shouldly; using TestStack.BDDfy; @@ -21,17 +23,17 @@ namespace Ocelot.AcceptanceTests { public class AuthenticationTests : IDisposable { - private TestServer _server; - private HttpClient _client; + private TestServer _ocelotServer; + private HttpClient _ocelotClient; private HttpResponseMessage _response; private readonly string _configurationPath; private StringContent _postContent; - private IWebHost _builder; + private IWebHost _ocelotBbuilder; // Sadly we need to change this when we update the netcoreapp version to make the test update the config correctly private double _netCoreAppVersion = 1.4; - private HttpClient _idServerClient; - private TestServer _idServer; + private BearerToken _token; + private IWebHost _identityServerBuilder; public AuthenticationTests() { @@ -39,7 +41,7 @@ namespace Ocelot.AcceptanceTests } [Fact] - public void should_return_401_using_jwt() + public void should_return_401_using_identity_server_access_token() { this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:51888")) .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51876", 201, string.Empty)) @@ -63,6 +65,33 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void should_return_201_using_identity_server_access_token() + { + this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:51888")) + .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51876", 201, string.Empty)) + .And(x => x.GivenIHaveAToken("http://localhost:51888")) + .And(x => x.GivenThereIsAConfiguration(new YamlConfiguration + { + ReRoutes = new List + { + new YamlReRoute + { + DownstreamTemplate = "http://localhost:51876/", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Post", + Authentication = "JwtBearerAuthentication" + } + } + })) + .And(x => x.GivenTheApiGatewayIsRunning()) + .And(x => x.GivenIHaveAddedATokenToMyRequest()) + .And(x => x.GivenThePostHasContent("postContent")) + .When(x => x.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => x.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .BDDfy(); + } + private void GivenThePostHasContent(string postcontent) { _postContent = new StringContent(postcontent); @@ -73,10 +102,10 @@ namespace Ocelot.AcceptanceTests /// private void GivenTheApiGatewayIsRunning() { - _server = new TestServer(new WebHostBuilder() + _ocelotServer = new TestServer(new WebHostBuilder() .UseStartup()); - _client = _server.CreateClient(); + _ocelotClient = _ocelotServer.CreateClient(); } private void GivenThereIsAConfiguration(YamlConfiguration yamlConfiguration) @@ -96,7 +125,7 @@ namespace Ocelot.AcceptanceTests private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) { - _builder = new WebHostBuilder() + _ocelotBbuilder = new WebHostBuilder() .UseUrls(url) .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) @@ -112,12 +141,12 @@ namespace Ocelot.AcceptanceTests }) .Build(); - _builder.Start(); + _ocelotBbuilder.Start(); } private void GivenThereIsAnIdentityServerOn(string url) { - var builder = new WebHostBuilder() + _identityServerBuilder = new WebHostBuilder() .UseUrls(url) .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) @@ -126,41 +155,51 @@ namespace Ocelot.AcceptanceTests .ConfigureServices(services => { services.AddDeveloperIdentityServer() - .AddInMemoryClients(new List { - new Client - { - ClientId = "test", - AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - ClientSecrets = new List { new Secret("test".Sha256()) }, - AllowedScopes = new List { "api1" }, - AllowAccessToAllScopes = true, - AccessTokenType = AccessTokenType.Jwt, - Enabled = true - - } }) .AddInMemoryScopes(new List { new Scope { - Name = "api1", + Name = "api", Description = "My API", Enabled = true }}) + .AddInMemoryClients(new List { + new Client + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List { new Secret("secret".Sha256()) }, + AllowedScopes = new List { "api" }, + AccessTokenType = AccessTokenType.Jwt, + Enabled = true, + RequireClientSecret = false + } }) .AddInMemoryUsers(new List { new InMemoryUser { - Username = "test", Password = "test", Enabled = true, Subject = "asdads" + Username = "test", + Password = "test", + Enabled = true, + Subject = "asdads" }}); }) .Configure(app => { app.UseIdentityServer(); - }); - - _idServer = new TestServer(builder); - _idServerClient = _idServer.CreateClient(); + }) + .Build(); - var response = _idServerClient.GetAsync($"{url}/.well-known/openid-configuration").Result; - response.EnsureSuccessStatusCode(); - var content = response.Content.ReadAsStringAsync().Result; + _identityServerBuilder.Start(); + + VerifyIdentiryServerStarted(url); + + } + + private void VerifyIdentiryServerStarted(string url) + { + using (var httpClient = new HttpClient()) + { + var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; + response.EnsureSuccessStatusCode(); + } } private void GivenIHaveAToken(string url) @@ -168,21 +207,32 @@ namespace Ocelot.AcceptanceTests var tokenUrl = $"{url}/connect/token"; var formData = new List> { - new KeyValuePair("client_id", "test"), - new KeyValuePair("client_secret", "test".Sha256()), - new KeyValuePair("scope", "api1"), + new KeyValuePair("client_id", "client"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "api"), new KeyValuePair("username", "test"), new KeyValuePair("password", "test"), new KeyValuePair("grant_type", "password") }; var content = new FormUrlEncodedContent(formData); - var response = _idServerClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; + + using (var httpClient = new HttpClient()) + { + var response = httpClient.PostAsync(tokenUrl, content).Result; + response.EnsureSuccessStatusCode(); + var responseContent = response.Content.ReadAsStringAsync().Result; + _token = JsonConvert.DeserializeObject(responseContent); + } + } + + private void GivenIHaveAddedATokenToMyRequest() + { + _ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); } private void WhenIPostUrlOnTheApiGateway(string url) { - _response = _client.PostAsync(url, _postContent).Result; + _response = _ocelotClient.PostAsync(url, _postContent).Result; } private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) @@ -192,11 +242,22 @@ namespace Ocelot.AcceptanceTests public void Dispose() { - _idServerClient?.Dispose(); - _idServer?.Dispose(); - _builder?.Dispose(); - _client.Dispose(); - _server.Dispose(); + _ocelotBbuilder?.Dispose(); + _ocelotClient?.Dispose(); + _ocelotServer?.Dispose(); + _identityServerBuilder?.Dispose(); + } + + class BearerToken + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + + [JsonProperty("token_type")] + public string TokenType { get; set; } } } } diff --git a/test/Ocelot.AcceptanceTests/configuration.yaml b/test/Ocelot.AcceptanceTests/configuration.yaml index 42e7ab07..c438428b 100644 --- a/test/Ocelot.AcceptanceTests/configuration.yaml +++ b/test/Ocelot.AcceptanceTests/configuration.yaml @@ -2,4 +2,4 @@ ReRoutes: - DownstreamTemplate: http://localhost:51879/ UpstreamTemplate: / UpstreamHttpMethod: Get - Authentication: IdentityServer + Authentication: IdentityServer.AccessToken diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationProviderFactoryTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationProviderFactoryTests.cs new file mode 100644 index 00000000..752aca0b --- /dev/null +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationProviderFactoryTests.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Library.Infrastructure.Authentication; +using Ocelot.Library.Infrastructure.Errors; +using Ocelot.Library.Infrastructure.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Authentication +{ + public class AuthenticationProviderFactoryTests + { + private readonly IAuthenticationProviderFactory _authenticationProviderFactory; + private readonly Mock _app; + private readonly Mock _creator; + + private string _provider; + private Response _result; + + public AuthenticationProviderFactoryTests() + { + _app = new Mock(); + _creator = new Mock(); + _authenticationProviderFactory = new AuthenticationProviderFactory(_creator.Object); + } + + [Fact] + public void should_return_identity_server_access_token_provider() + { + this.Given(x => x.GivenTheProviderIs("IdentityServer.AccessToken")) + .And(x => x.GivenTheCreatorReturns()) + .When(x => x.WhenIGetFromTheFactory()) + .Then(x => x.ThenTheHandlerIsReturned("IdentityServer.AccessToken")) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_cannot_create_handler() + { + this.Given(x => x.GivenTheProviderIs("IdentityServer.AccessToken")) + .And(x => x.GivenTheCreatorReturnsAnError()) + .When(x => x.WhenIGetFromTheFactory()) + .Then(x => x.ThenAnErrorResponseIsReturned()) + .BDDfy(); + } + + private void GivenTheCreatorReturnsAnError() + { + _creator + .Setup(x => x.CreateIdentityServerAuthenticationHandler(It.IsAny())) + .Returns(new ErrorResponse(new List + { + new UnableToCreateAuthenticationHandlerError($"Unable to create authentication handler for xxx") + })); + } + + private void GivenTheCreatorReturns() + { + _creator + .Setup(x => x.CreateIdentityServerAuthenticationHandler(It.IsAny())) + .Returns(new OkResponse(x => Task.CompletedTask)); + } + + private void GivenTheProviderIs(string provider) + { + _provider = provider; + } + + private void WhenIGetFromTheFactory() + { + _result = _authenticationProviderFactory.Get(_provider, _app.Object); + } + + private void ThenTheHandlerIsReturned(string expected) + { + _result.Data.Provider.ShouldBe(expected); + } + + private void ThenAnErrorResponseIsReturned() + { + _result.IsError.ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.UnitTests/Middleware/AuthenticationMiddlewareTests.cs b/test/Ocelot.UnitTests/Middleware/AuthenticationMiddlewareTests.cs index fb4e2d32..6c82b0fe 100644 --- a/test/Ocelot.UnitTests/Middleware/AuthenticationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Middleware/AuthenticationMiddlewareTests.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Moq; +using Ocelot.Library.Infrastructure.Authentication; using Ocelot.Library.Infrastructure.DownstreamRouteFinder; using Ocelot.Library.Infrastructure.Middleware; using Ocelot.Library.Infrastructure.Repository; @@ -21,6 +22,7 @@ namespace Ocelot.UnitTests.Middleware public class AuthenticationMiddlewareTests : IDisposable { private readonly Mock _scopedRepository; + private readonly Mock _authFactory; private readonly string _url; private readonly TestServer _server; private readonly HttpClient _client; @@ -31,10 +33,11 @@ namespace Ocelot.UnitTests.Middleware { _url = "http://localhost:51879"; _scopedRepository = new Mock(); - + _authFactory = new Mock(); var builder = new WebHostBuilder() .ConfigureServices(x => { + x.AddSingleton(_authFactory.Object); x.AddSingleton(_scopedRepository.Object); }) .UseUrls(_url)