diff --git a/README.md b/README.md index d0db1e39..d2316c9a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ [![Join the chat at https://gitter.im/Ocelotey/Lobby](https://badges.gitter.im/Ocelotey/Lobby.svg)](https://gitter.im/Ocelotey/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![](https://codescene.io/projects/697/status.svg) Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results) + Attempt at a .NET Api Gateway This project is aimed at people using .NET running @@ -73,7 +75,7 @@ More information on how to use these options is below.. An example startup using a json file for configuration can be seen below. Currently this is the only way to get configuration into Ocelot. - public class Startup + public class Startup { public Startup(IHostingEnvironment env) { @@ -101,15 +103,14 @@ Currently this is the only way to get configuration into Ocelot. }; services.AddOcelotOutputCaching(settings); - services.AddOcelotFileConfiguration(Configuration); - services.AddOcelot(); + services.AddOcelot(Configuration); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); - app.UseOcelot(); + await app.UseOcelot(); } } @@ -386,6 +387,43 @@ In orde to use caching on a route in your ReRoute configuration add this setting In this example ttl seconds is set to 15 which means the cache will expire after 15 seconds. +## Administration + +Ocelot supports changing configuration during runtime via an authenticated HTTP API. The API is authenticated +using bearer tokens that you request from iteself. This support is provided by the amazing IdentityServer +project that I have been using for a few years now. Check them out. + +In order to enable the administration section you need to do a few things. First of all add this to your +initial configuration.json. The value can be anything you want and it is obviously reccomended don't use +a url you would like to route through with Ocelot as this will not work. The administration uses the +MapWhen functionality of asp.net core and all requests to root/administration will be sent there not +to the Ocelot middleware. + + "GlobalConfiguration": { + "AdministrationPath": "/administration" + } + +This will get the admin area set up but not the authentication. You need to set 3 environmental variables. + + OCELOT_USERNAME + OCELOT_HASH + OCELOT_SALT + +These need to be the admin username you want to use with Ocelot and the hash and salt of the password you want to +use given hashing algorythm. When requesting bearer tokens for use with the administration api you will need to +supply username and password. + +In order to create a hash and salt of your password please check out HashCreationTests.should_create_hash_and_salt() this technique is based on MS doc I found online TODO find and link... + +OK next thing is to get this config into Ocelot... + + +At the moment Ocelot supports really limited options in terms of users and authentication for the admin API. At +least your stuff needs to be hashed! + + + + ## Ocelot Middleware injection and overrides Warning use with caution. If you are seeing any exceptions or strange behavior in your middleware diff --git a/configuration.json b/configuration.json index 2faeadfa..3f39532c 100755 --- a/configuration.json +++ b/configuration.json @@ -1 +1 @@ -{"ReRoutes":[{"DownstreamPathTemplate":"/","UpstreamPathTemplate":"/","UpstreamHttpMethod":"get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"https","DownstreamHost":"localhost","DownstreamPort":80,"QoSOptions":{"ExceptionsAllowedBeforeBreaking":0,"DurationOfBreak":0,"TimeoutValue":0},"LoadBalancer":null},{"DownstreamPathTemplate":"/","UpstreamPathTemplate":"/test","UpstreamHttpMethod":"get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"https","DownstreamHost":"localhost","DownstreamPort":80,"QoSOptions":{"ExceptionsAllowedBeforeBreaking":0,"DurationOfBreak":0,"TimeoutValue":0},"LoadBalancer":null}],"GlobalConfiguration":{"RequestIdKey":"RequestId","ServiceDiscoveryProvider":{"Provider":"test","Host":"127.0.0.1","Port":0},"AdministrationPath":"/administration"}} \ No newline at end of file +{"ReRoutes":[],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"AdministrationPath":"/administration"}} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Authentication/HashMatcher.cs b/src/Ocelot/Configuration/Authentication/HashMatcher.cs new file mode 100644 index 00000000..08773332 --- /dev/null +++ b/src/Ocelot/Configuration/Authentication/HashMatcher.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; + +namespace Ocelot.Configuration.Authentication +{ + public class HashMatcher : IHashMatcher + { + public bool Match(string password, string salt, string hash) + { + byte[] s = Convert.FromBase64String(salt); + + string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2( + password: password, + salt: s, + prf: KeyDerivationPrf.HMACSHA256, + iterationCount: 10000, + numBytesRequested: 256 / 8)); + + return hashed == hash; + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Authentication/IHashMatcher.cs b/src/Ocelot/Configuration/Authentication/IHashMatcher.cs new file mode 100644 index 00000000..ad0d8e03 --- /dev/null +++ b/src/Ocelot/Configuration/Authentication/IHashMatcher.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Configuration.Authentication +{ + public interface IHashMatcher + { + bool Match(string password, string salt, string hash); + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Authentication/OcelotResourceOwnerPasswordValidator.cs b/src/Ocelot/Configuration/Authentication/OcelotResourceOwnerPasswordValidator.cs new file mode 100644 index 00000000..416c8ec2 --- /dev/null +++ b/src/Ocelot/Configuration/Authentication/OcelotResourceOwnerPasswordValidator.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using IdentityServer4.Models; +using IdentityServer4.Validation; +using Ocelot.Configuration.Provider; + +namespace Ocelot.Configuration.Authentication +{ + public class OcelotResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator + { + private readonly IHashMatcher _matcher; + private readonly IIdentityServerConfiguration _identityServerConfiguration; + + public OcelotResourceOwnerPasswordValidator(IHashMatcher matcher, IIdentityServerConfiguration identityServerConfiguration) + { + _identityServerConfiguration = identityServerConfiguration; + _matcher = matcher; + } + + public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) + { + try + { + var user = _identityServerConfiguration.Users.FirstOrDefault(u => u.UserName == context.UserName); + + if(user == null) + { + context.Result = new GrantValidationResult( + TokenRequestErrors.InvalidGrant, + "invalid custom credential"); + } + else if(_matcher.Match(context.Password, user.Salt, user.Hash)) + { + context.Result = new GrantValidationResult( + subject: "admin", + authenticationMethod: "custom"); + } + else + { + context.Result = new GrantValidationResult( + TokenRequestErrors.InvalidGrant, + "invalid custom credential"); + } + } + catch(Exception ex) + { + Console.WriteLine(ex); + } + + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs b/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs new file mode 100644 index 00000000..bb66265f --- /dev/null +++ b/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; + +namespace Ocelot.Configuration.Provider +{ + public interface IIdentityServerConfiguration + { + string ApiName { get; } + bool RequireHttps { get; } + List AllowedScopes { get; } + SupportedTokens SupportedTokens { get; } + string ApiSecret { get; } + string Description {get;} + bool Enabled {get;} + IEnumerable AllowedGrantTypes {get;} + AccessTokenType AccessTokenType {get;} + bool RequireClientSecret {get;} + List Users {get;} + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Provider/HardCodedIdentityServerConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs similarity index 52% rename from src/Ocelot/Configuration/Provider/HardCodedIdentityServerConfigurationProvider.cs rename to src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs index 09780bc6..f0f6897d 100644 --- a/src/Ocelot/Configuration/Provider/HardCodedIdentityServerConfigurationProvider.cs +++ b/src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs @@ -1,49 +1,12 @@ -using System; using System.Collections.Generic; using IdentityServer4.AccessTokenValidation; using IdentityServer4.Models; -using IdentityServer4.Test; namespace Ocelot.Configuration.Provider { - public class HardCodedIdentityServerConfigurationProvider : IIdentityServerConfigurationProvider - { - public IdentityServerConfiguration Get() - { - var url = ""; - return new IdentityServerConfiguration( - url, - "admin", - false, - SupportedTokens.Both, - "secret", - new List {"admin", "openid", "offline_access"}, - "Ocelot Administration", - true, - GrantTypes.ResourceOwnerPassword, - AccessTokenType.Jwt, - false, - new List { - new TestUser - { - Username = "admin", - Password = "admin", - SubjectId = "admin", - } - } - ); - } - } - - public interface IIdentityServerConfigurationProvider - { - IdentityServerConfiguration Get(); - } - - public class IdentityServerConfiguration + public class IdentityServerConfiguration : IIdentityServerConfiguration { public IdentityServerConfiguration( - string identityServerUrl, string apiName, bool requireHttps, SupportedTokens supportedTokens, @@ -54,9 +17,8 @@ namespace Ocelot.Configuration.Provider IEnumerable grantType, AccessTokenType accessTokenType, bool requireClientSecret, - List users) + List users) { - IdentityServerUrl = identityServerUrl; ApiName = apiName; RequireHttps = requireHttps; SupportedTokens = supportedTokens; @@ -70,7 +32,6 @@ namespace Ocelot.Configuration.Provider Users = users; } - public string IdentityServerUrl { get; private set; } public string ApiName { get; private set; } public bool RequireHttps { get; private set; } public List AllowedScopes { get; private set; } @@ -80,7 +41,7 @@ namespace Ocelot.Configuration.Provider public bool Enabled {get;private set;} public IEnumerable AllowedGrantTypes {get;private set;} public AccessTokenType AccessTokenType {get;private set;} - public bool RequireClientSecret = false; - public List Users {get;private set;} + public bool RequireClientSecret {get;private set;} + public List Users {get;private set;} } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/Provider/User.cs b/src/Ocelot/Configuration/Provider/User.cs new file mode 100644 index 00000000..f61ff4e5 --- /dev/null +++ b/src/Ocelot/Configuration/Provider/User.cs @@ -0,0 +1,17 @@ +namespace Ocelot.Configuration.Provider +{ + public class User + { + public User(string subject, string userName, string hash, string salt) + { + Subject = subject; + UserName = userName; + Hash = hash; + Salt = salt; + } + public string Subject { get; private set; } + public string UserName { get; private set; } + public string Hash { get; private set; } + public string Salt { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index 52c47a25..ccd63e64 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -14,6 +14,7 @@ using Ocelot.Authentication.Handler.Factory; using Ocelot.Authorisation; using Ocelot.Cache; using Ocelot.Claims; +using Ocelot.Configuration.Authentication; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; @@ -41,7 +42,6 @@ namespace Ocelot.DependencyInjection { public static class ServiceCollectionExtensions { - public static IServiceCollection AddOcelotOutputCaching(this IServiceCollection services, Action settings) { var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", settings); @@ -51,24 +51,23 @@ namespace Ocelot.DependencyInjection return services; } - public static IServiceCollection AddOcelotFileConfiguration(this IServiceCollection services, IConfigurationRoot configurationRoot) + + public static IServiceCollection AddOcelot(this IServiceCollection services, IConfigurationRoot configurationRoot) + { + return AddOcelot(services, configurationRoot, null); + } + + public static IServiceCollection AddOcelot(this IServiceCollection services, IConfigurationRoot configurationRoot, IIdentityServerConfiguration identityServerConfiguration) { services.Configure(configurationRoot); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - return services; - } - - public static IServiceCollection AddOcelot(this IServiceCollection services) - { - return AddOcelot(services, null); - } - - public static IServiceCollection AddOcelot(this IServiceCollection services, IdentityServerConfiguration identityServerConfiguration) - { + if(identityServerConfiguration != null) { + services.AddSingleton(identityServerConfiguration); + services.AddSingleton(); services.AddIdentityServer() .AddTemporarySigningCredential() .AddInMemoryApiResources(new List @@ -101,8 +100,7 @@ namespace Ocelot.DependencyInjection Enabled = identityServerConfiguration.Enabled, RequireClientSecret = identityServerConfiguration.RequireClientSecret } - }) - .AddTestUsers(identityServerConfiguration.Users); + }).AddResourceOwnerValidator(); } services.AddMvcCore() diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index 99509650..553b05b8 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -37,21 +37,7 @@ namespace Ocelot.Middleware /// public static async Task UseOcelot(this IApplicationBuilder builder) { - await builder.UseOcelot(new OcelotMiddlewareConfiguration(), null); - - return builder; - } - - public static async Task UseOcelot(this IApplicationBuilder builder,IdentityServerConfiguration identityServerConfiguration) - { - await builder.UseOcelot(new OcelotMiddlewareConfiguration(), identityServerConfiguration); - - return builder; - } - - public static async Task UseOcelot(this IApplicationBuilder builder,OcelotMiddlewareConfiguration middlewareConfiguration) - { - await builder.UseOcelot(middlewareConfiguration, null); + await builder.UseOcelot(new OcelotMiddlewareConfiguration()); return builder; } @@ -62,9 +48,9 @@ namespace Ocelot.Middleware /// /// /// - public static async Task UseOcelot(this IApplicationBuilder builder, OcelotMiddlewareConfiguration middlewareConfiguration, IdentityServerConfiguration identityServerConfiguration) + public static async Task UseOcelot(this IApplicationBuilder builder, OcelotMiddlewareConfiguration middlewareConfiguration) { - await CreateAdministrationArea(builder, identityServerConfiguration); + await CreateAdministrationArea(builder); // This is registered to catch any global exceptions that are not handled builder.UseExceptionHandlerMiddleware(); @@ -168,10 +154,12 @@ namespace Ocelot.Middleware return ocelotConfiguration.Data; } - private static async Task CreateAdministrationArea(IApplicationBuilder builder, IdentityServerConfiguration identityServerConfiguration) + private static async Task CreateAdministrationArea(IApplicationBuilder builder) { var configuration = await CreateConfiguration(builder); + var identityServerConfiguration = (IIdentityServerConfiguration)builder.ApplicationServices.GetService(typeof(IIdentityServerConfiguration)); + if(!string.IsNullOrEmpty(configuration.AdministrationPath) && identityServerConfiguration != null) { var webHostBuilder = (IWebHostBuilder)builder.ApplicationServices.GetService(typeof(IWebHostBuilder)); diff --git a/src/Ocelot/project.json b/src/Ocelot/project.json index 1cb4b0c9..1ca9d403 100644 --- a/src/Ocelot/project.json +++ b/src/Ocelot/project.json @@ -29,7 +29,8 @@ "CacheManager.Microsoft.Extensions.Logging": "0.9.2", "Consul": "0.7.2.1", "Polly": "5.0.3", - "IdentityServer4": "1.0.1" + "IdentityServer4": "1.0.1", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0" }, "runtimes": { "win10-x64": {}, diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index d923148a..5a256c81 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -139,8 +139,7 @@ namespace Ocelot.AcceptanceTests }; s.AddOcelotOutputCaching(settings); - s.AddOcelotFileConfiguration(configuration); - s.AddOcelot(); + s.AddOcelot(configuration); }) .ConfigureLogging(l => { diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 06e7bd5d..210c7ac5 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -235,7 +235,7 @@ namespace Ocelot.IntegrationTests new KeyValuePair("client_secret", "secret"), new KeyValuePair("scope", "admin"), new KeyValuePair("username", "admin"), - new KeyValuePair("password", "admin"), + new KeyValuePair("password", "secret"), new KeyValuePair("grant_type", "password") }; var content = new FormUrlEncodedContent(formData); diff --git a/test/Ocelot.ManualTest/Startup.cs b/test/Ocelot.ManualTest/Startup.cs index aa34c9a2..c1277af7 100644 --- a/test/Ocelot.ManualTest/Startup.cs +++ b/test/Ocelot.ManualTest/Startup.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using CacheManager.Core; +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -15,7 +18,7 @@ namespace Ocelot.ManualTest { public class Startup { - private IdentityServerConfiguration _identityServerConfig; + private IIdentityServerConfiguration _identityServerConfig; public Startup(IHostingEnvironment env) { @@ -27,9 +30,6 @@ namespace Ocelot.ManualTest .AddEnvironmentVariables(); Configuration = builder.Build(); - - var identityServerConfigProvider = new HardCodedIdentityServerConfigurationProvider(); - _identityServerConfig = identityServerConfigProvider.Get(); } public IConfigurationRoot Configuration { get; } @@ -46,15 +46,36 @@ namespace Ocelot.ManualTest }; services.AddOcelotOutputCaching(settings); - services.AddOcelotFileConfiguration(Configuration); - services.AddOcelot(_identityServerConfig); + + var username = Environment.GetEnvironmentVariable("OCELOT_USERNAME"); + var hash = Environment.GetEnvironmentVariable("OCELOT_HASH"); + var salt = Environment.GetEnvironmentVariable("OCELOT_SALT"); + + _identityServerConfig = new IdentityServerConfiguration( + "admin", + false, + SupportedTokens.Both, + "secret", + new List {"admin", "openid", "offline_access"}, + "Ocelot Administration", + true, + GrantTypes.ResourceOwnerPassword, + AccessTokenType.Jwt, + false, + new List + { + new User("admin", username, hash, salt) + } + ); + + services.AddOcelot(Configuration, _identityServerConfig); } public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); - await app.UseOcelot(_identityServerConfig); + await app.UseOcelot(); } } } diff --git a/test/Ocelot.ManualTest/project.json b/test/Ocelot.ManualTest/project.json index a31fdca2..52a8962f 100644 --- a/test/Ocelot.ManualTest/project.json +++ b/test/Ocelot.ManualTest/project.json @@ -14,7 +14,8 @@ "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", "Microsoft.NETCore.App": "1.1.0", "Consul": "0.7.2.1", - "Polly": "5.0.3" + "Polly": "5.0.3", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0" }, "tools": { diff --git a/test/Ocelot.UnitTests/Configuration/HashCreationTests.cs b/test/Ocelot.UnitTests/Configuration/HashCreationTests.cs new file mode 100644 index 00000000..d99efd58 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/HashCreationTests.cs @@ -0,0 +1,33 @@ +using System; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using Shouldly; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class HashCreationTests + { + [Fact] + public void should_create_hash_and_salt() + { + var password = "secret"; + + var salt = new byte[128 / 8]; + + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(salt); + } + + var storedSalt = Convert.ToBase64String(salt); + + var storedHash = Convert.ToBase64String(KeyDerivation.Pbkdf2( + password: password, + salt: salt, + prf: KeyDerivationPrf.HMACSHA256, + iterationCount: 10000, + numBytesRequested: 256 / 8)); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/HashMatcherTests.cs b/test/Ocelot.UnitTests/Configuration/HashMatcherTests.cs new file mode 100644 index 00000000..34e9477e --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/HashMatcherTests.cs @@ -0,0 +1,76 @@ +using Ocelot.Configuration.Authentication; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class HashMatcherTests + { + private string _password; + private string _hash; + private string _salt; + private bool _result; + private HashMatcher _hashMatcher; + + public HashMatcherTests() + { + _hashMatcher = new HashMatcher(); + } + + [Fact] + public void should_match_hash() + { + var hash = "kE/mxd1hO9h9Sl2VhGhwJUd9xZEv4NP6qXoN39nIqM4="; + var salt = "zzWITpnDximUNKYLiUam/w=="; + var password = "secret"; + + this.Given(x => GivenThePassword(password)) + .And(x => GivenTheHash(hash)) + .And(x => GivenTheSalt(salt)) + .When(x => WhenIMatch()) + .Then(x => ThenTheResultIs(true)) + .BDDfy(); + } + + [Fact] + public void should_not_match_hash() + { + var hash = "kE/mxd1hO9h9Sl2VhGhwJUd9xZEv4NP6qXoN39nIqM4="; + var salt = "zzWITpnDximUNKYLiUam/w=="; + var password = "secret1"; + + this.Given(x => GivenThePassword(password)) + .And(x => GivenTheHash(hash)) + .And(x => GivenTheSalt(salt)) + .When(x => WhenIMatch()) + .Then(x => ThenTheResultIs(false)) + .BDDfy(); + } + + private void GivenThePassword(string password) + { + _password = password; + } + + private void GivenTheHash(string hash) + { + _hash = hash; + } + + private void GivenTheSalt(string salt) + { + _salt = salt; + } + + private void WhenIMatch() + { + _result = _hashMatcher.Match(_password, _salt, _hash); + } + + private void ThenTheResultIs(bool expected) + { + _result.ShouldBe(expected); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/OcelotResourceOwnerPasswordValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/OcelotResourceOwnerPasswordValidatorTests.cs new file mode 100644 index 00000000..a8d11713 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/OcelotResourceOwnerPasswordValidatorTests.cs @@ -0,0 +1,117 @@ +using Ocelot.Configuration.Authentication; +using Xunit; +using Shouldly; +using TestStack.BDDfy; +using Moq; +using IdentityServer4.Validation; +using Ocelot.Configuration.Provider; +using System.Collections.Generic; + +namespace Ocelot.UnitTests.Configuration +{ + public class OcelotResourceOwnerPasswordValidatorTests + { + private OcelotResourceOwnerPasswordValidator _validator; + private Mock _matcher; + private string _userName; + private string _password; + private ResourceOwnerPasswordValidationContext _context; + private Mock _config; + private User _user; + + public OcelotResourceOwnerPasswordValidatorTests() + { + _matcher = new Mock(); + _config = new Mock(); + _validator = new OcelotResourceOwnerPasswordValidator(_matcher.Object, _config.Object); + } + + [Fact] + public void should_return_success() + { + this.Given(x => GivenTheUserName("tom")) + .And(x => GivenThePassword("password")) + .And(x => GivenTheUserIs(new User("sub", "tom", "xxx", "xxx"))) + .And(x => GivenTheMatcherReturns(true)) + .When(x => WhenIValidate()) + .Then(x => ThenTheUserIsValidated()) + .And(x => ThenTheMatcherIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_fail_when_no_user() + { + this.Given(x => GivenTheUserName("bob")) + .And(x => GivenTheUserIs(new User("sub", "tom", "xxx", "xxx"))) + .And(x => GivenTheMatcherReturns(true)) + .When(x => WhenIValidate()) + .Then(x => ThenTheUserIsNotValidated()) + .BDDfy(); + } + + [Fact] + public void should_return_fail_when_password_doesnt_match() + { + this.Given(x => GivenTheUserName("tom")) + .And(x => GivenThePassword("password")) + .And(x => GivenTheUserIs(new User("sub", "tom", "xxx", "xxx"))) + .And(x => GivenTheMatcherReturns(false)) + .When(x => WhenIValidate()) + .Then(x => ThenTheUserIsNotValidated()) + .And(x => ThenTheMatcherIsCalledCorrectly()) + .BDDfy(); + } + + private void ThenTheMatcherIsCalledCorrectly() + { + _matcher + .Verify(x => x.Match(_password, _user.Salt, _user.Hash), Times.Once); + } + + private void GivenThePassword(string password) + { + _password = password; + } + + private void GivenTheUserIs(User user) + { + _user = user; + _config + .Setup(x => x.Users) + .Returns(new List{_user}); + } + + private void GivenTheMatcherReturns(bool expected) + { + _matcher + .Setup(x => x.Match(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(expected); + } + + private void GivenTheUserName(string userName) + { + _userName = userName; + } + + private void WhenIValidate() + { + _context = new ResourceOwnerPasswordValidationContext + { + UserName = _userName, + Password = _password + }; + _validator.ValidateAsync(_context).Wait(); + } + + private void ThenTheUserIsValidated() + { + _context.Result.IsError.ShouldBe(false); + } + + private void ThenTheUserIsNotValidated() + { + _context.Result.IsError.ShouldBe(true); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/project.json b/test/Ocelot.UnitTests/project.json index e61760d6..bbc706b6 100644 --- a/test/Ocelot.UnitTests/project.json +++ b/test/Ocelot.UnitTests/project.json @@ -24,7 +24,8 @@ "Shouldly": "2.8.2", "TestStack.BDDfy": "4.3.2", "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0", - "Microsoft.DotNet.InternalAbstractions": "1.0.0" + "Microsoft.DotNet.InternalAbstractions": "1.0.0", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0" }, "runtimes": { "win10-x64": {},