hacky auth working

This commit is contained in:
Tom Gardham-Pallister 2017-02-24 19:52:48 +00:00
parent bd07af6926
commit 112a9c303e
19 changed files with 448 additions and 94 deletions

View File

@ -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) [![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 Attempt at a .NET Api Gateway
This project is aimed at people using .NET running This project is aimed at people using .NET running
@ -101,15 +103,14 @@ Currently this is the only way to get configuration into Ocelot.
}; };
services.AddOcelotOutputCaching(settings); services.AddOcelotOutputCaching(settings);
services.AddOcelotFileConfiguration(Configuration); services.AddOcelot(Configuration);
services.AddOcelot();
} }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{ {
loggerFactory.AddConsole(Configuration.GetSection("Logging")); 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. 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 ## Ocelot Middleware injection and overrides
Warning use with caution. If you are seeing any exceptions or strange behavior in your middleware Warning use with caution. If you are seeing any exceptions or strange behavior in your middleware

View File

@ -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"}} {"ReRoutes":[],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"AdministrationPath":"/administration"}}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,7 @@
namespace Ocelot.Configuration.Authentication
{
public interface IHashMatcher
{
bool Match(string password, string salt, string hash);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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<string> AllowedScopes { get; }
SupportedTokens SupportedTokens { get; }
string ApiSecret { get; }
string Description {get;}
bool Enabled {get;}
IEnumerable<string> AllowedGrantTypes {get;}
AccessTokenType AccessTokenType {get;}
bool RequireClientSecret {get;}
List<User> Users {get;}
}
}

View File

@ -1,49 +1,12 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using IdentityServer4.AccessTokenValidation; using IdentityServer4.AccessTokenValidation;
using IdentityServer4.Models; using IdentityServer4.Models;
using IdentityServer4.Test;
namespace Ocelot.Configuration.Provider namespace Ocelot.Configuration.Provider
{ {
public class HardCodedIdentityServerConfigurationProvider : IIdentityServerConfigurationProvider public class IdentityServerConfiguration : IIdentityServerConfiguration
{
public IdentityServerConfiguration Get()
{
var url = "";
return new IdentityServerConfiguration(
url,
"admin",
false,
SupportedTokens.Both,
"secret",
new List<string> {"admin", "openid", "offline_access"},
"Ocelot Administration",
true,
GrantTypes.ResourceOwnerPassword,
AccessTokenType.Jwt,
false,
new List<TestUser> {
new TestUser
{
Username = "admin",
Password = "admin",
SubjectId = "admin",
}
}
);
}
}
public interface IIdentityServerConfigurationProvider
{
IdentityServerConfiguration Get();
}
public class IdentityServerConfiguration
{ {
public IdentityServerConfiguration( public IdentityServerConfiguration(
string identityServerUrl,
string apiName, string apiName,
bool requireHttps, bool requireHttps,
SupportedTokens supportedTokens, SupportedTokens supportedTokens,
@ -54,9 +17,8 @@ namespace Ocelot.Configuration.Provider
IEnumerable<string> grantType, IEnumerable<string> grantType,
AccessTokenType accessTokenType, AccessTokenType accessTokenType,
bool requireClientSecret, bool requireClientSecret,
List<TestUser> users) List<User> users)
{ {
IdentityServerUrl = identityServerUrl;
ApiName = apiName; ApiName = apiName;
RequireHttps = requireHttps; RequireHttps = requireHttps;
SupportedTokens = supportedTokens; SupportedTokens = supportedTokens;
@ -70,7 +32,6 @@ namespace Ocelot.Configuration.Provider
Users = users; Users = users;
} }
public string IdentityServerUrl { get; private set; }
public string ApiName { get; private set; } public string ApiName { get; private set; }
public bool RequireHttps { get; private set; } public bool RequireHttps { get; private set; }
public List<string> AllowedScopes { get; private set; } public List<string> AllowedScopes { get; private set; }
@ -80,7 +41,7 @@ namespace Ocelot.Configuration.Provider
public bool Enabled {get;private set;} public bool Enabled {get;private set;}
public IEnumerable<string> AllowedGrantTypes {get;private set;} public IEnumerable<string> AllowedGrantTypes {get;private set;}
public AccessTokenType AccessTokenType {get;private set;} public AccessTokenType AccessTokenType {get;private set;}
public bool RequireClientSecret = false; public bool RequireClientSecret {get;private set;}
public List<TestUser> Users {get;private set;} public List<User> Users {get;private set;}
} }
} }

View File

@ -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; }
}
}

View File

@ -14,6 +14,7 @@ using Ocelot.Authentication.Handler.Factory;
using Ocelot.Authorisation; using Ocelot.Authorisation;
using Ocelot.Cache; using Ocelot.Cache;
using Ocelot.Claims; using Ocelot.Claims;
using Ocelot.Configuration.Authentication;
using Ocelot.Configuration.Creator; using Ocelot.Configuration.Creator;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using Ocelot.Configuration.Parser; using Ocelot.Configuration.Parser;
@ -41,7 +42,6 @@ namespace Ocelot.DependencyInjection
{ {
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {
public static IServiceCollection AddOcelotOutputCaching(this IServiceCollection services, Action<ConfigurationBuilderCachePart> settings) public static IServiceCollection AddOcelotOutputCaching(this IServiceCollection services, Action<ConfigurationBuilderCachePart> settings)
{ {
var cacheManagerOutputCache = CacheFactory.Build<HttpResponseMessage>("OcelotOutputCache", settings); var cacheManagerOutputCache = CacheFactory.Build<HttpResponseMessage>("OcelotOutputCache", settings);
@ -51,24 +51,23 @@ namespace Ocelot.DependencyInjection
return services; 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<FileConfiguration>(configurationRoot); services.Configure<FileConfiguration>(configurationRoot);
services.AddSingleton<IOcelotConfigurationCreator, FileOcelotConfigurationCreator>(); services.AddSingleton<IOcelotConfigurationCreator, FileOcelotConfigurationCreator>();
services.AddSingleton<IOcelotConfigurationRepository, InMemoryOcelotConfigurationRepository>(); services.AddSingleton<IOcelotConfigurationRepository, InMemoryOcelotConfigurationRepository>();
services.AddSingleton<IConfigurationValidator, FileConfigurationValidator>(); services.AddSingleton<IConfigurationValidator, FileConfigurationValidator>();
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) if(identityServerConfiguration != null)
{ {
services.AddSingleton<IIdentityServerConfiguration>(identityServerConfiguration);
services.AddSingleton<IHashMatcher, HashMatcher>();
services.AddIdentityServer() services.AddIdentityServer()
.AddTemporarySigningCredential() .AddTemporarySigningCredential()
.AddInMemoryApiResources(new List<ApiResource> .AddInMemoryApiResources(new List<ApiResource>
@ -101,8 +100,7 @@ namespace Ocelot.DependencyInjection
Enabled = identityServerConfiguration.Enabled, Enabled = identityServerConfiguration.Enabled,
RequireClientSecret = identityServerConfiguration.RequireClientSecret RequireClientSecret = identityServerConfiguration.RequireClientSecret
} }
}) }).AddResourceOwnerValidator<OcelotResourceOwnerPasswordValidator>();
.AddTestUsers(identityServerConfiguration.Users);
} }
services.AddMvcCore() services.AddMvcCore()

View File

@ -37,21 +37,7 @@ namespace Ocelot.Middleware
/// <returns></returns> /// <returns></returns>
public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder) public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder)
{ {
await builder.UseOcelot(new OcelotMiddlewareConfiguration(), null); await builder.UseOcelot(new OcelotMiddlewareConfiguration());
return builder;
}
public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder,IdentityServerConfiguration identityServerConfiguration)
{
await builder.UseOcelot(new OcelotMiddlewareConfiguration(), identityServerConfiguration);
return builder;
}
public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder,OcelotMiddlewareConfiguration middlewareConfiguration)
{
await builder.UseOcelot(middlewareConfiguration, null);
return builder; return builder;
} }
@ -62,9 +48,9 @@ namespace Ocelot.Middleware
/// <param name="builder"></param> /// <param name="builder"></param>
/// <param name="middlewareConfiguration"></param> /// <param name="middlewareConfiguration"></param>
/// <returns></returns> /// <returns></returns>
public static async Task<IApplicationBuilder> UseOcelot(this IApplicationBuilder builder, OcelotMiddlewareConfiguration middlewareConfiguration, IdentityServerConfiguration identityServerConfiguration) public static async Task<IApplicationBuilder> 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 // This is registered to catch any global exceptions that are not handled
builder.UseExceptionHandlerMiddleware(); builder.UseExceptionHandlerMiddleware();
@ -168,10 +154,12 @@ namespace Ocelot.Middleware
return ocelotConfiguration.Data; 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 configuration = await CreateConfiguration(builder);
var identityServerConfiguration = (IIdentityServerConfiguration)builder.ApplicationServices.GetService(typeof(IIdentityServerConfiguration));
if(!string.IsNullOrEmpty(configuration.AdministrationPath) && identityServerConfiguration != null) if(!string.IsNullOrEmpty(configuration.AdministrationPath) && identityServerConfiguration != null)
{ {
var webHostBuilder = (IWebHostBuilder)builder.ApplicationServices.GetService(typeof(IWebHostBuilder)); var webHostBuilder = (IWebHostBuilder)builder.ApplicationServices.GetService(typeof(IWebHostBuilder));

View File

@ -29,7 +29,8 @@
"CacheManager.Microsoft.Extensions.Logging": "0.9.2", "CacheManager.Microsoft.Extensions.Logging": "0.9.2",
"Consul": "0.7.2.1", "Consul": "0.7.2.1",
"Polly": "5.0.3", "Polly": "5.0.3",
"IdentityServer4": "1.0.1" "IdentityServer4": "1.0.1",
"Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0"
}, },
"runtimes": { "runtimes": {
"win10-x64": {}, "win10-x64": {},

View File

@ -139,8 +139,7 @@ namespace Ocelot.AcceptanceTests
}; };
s.AddOcelotOutputCaching(settings); s.AddOcelotOutputCaching(settings);
s.AddOcelotFileConfiguration(configuration); s.AddOcelot(configuration);
s.AddOcelot();
}) })
.ConfigureLogging(l => .ConfigureLogging(l =>
{ {

View File

@ -235,7 +235,7 @@ namespace Ocelot.IntegrationTests
new KeyValuePair<string, string>("client_secret", "secret"), new KeyValuePair<string, string>("client_secret", "secret"),
new KeyValuePair<string, string>("scope", "admin"), new KeyValuePair<string, string>("scope", "admin"),
new KeyValuePair<string, string>("username", "admin"), new KeyValuePair<string, string>("username", "admin"),
new KeyValuePair<string, string>("password", "admin"), new KeyValuePair<string, string>("password", "secret"),
new KeyValuePair<string, string>("grant_type", "password") new KeyValuePair<string, string>("grant_type", "password")
}; };
var content = new FormUrlEncodedContent(formData); var content = new FormUrlEncodedContent(formData);

View File

@ -1,6 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using CacheManager.Core; using CacheManager.Core;
using IdentityServer4.AccessTokenValidation;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -15,7 +18,7 @@ namespace Ocelot.ManualTest
{ {
public class Startup public class Startup
{ {
private IdentityServerConfiguration _identityServerConfig; private IIdentityServerConfiguration _identityServerConfig;
public Startup(IHostingEnvironment env) public Startup(IHostingEnvironment env)
{ {
@ -27,9 +30,6 @@ namespace Ocelot.ManualTest
.AddEnvironmentVariables(); .AddEnvironmentVariables();
Configuration = builder.Build(); Configuration = builder.Build();
var identityServerConfigProvider = new HardCodedIdentityServerConfigurationProvider();
_identityServerConfig = identityServerConfigProvider.Get();
} }
public IConfigurationRoot Configuration { get; } public IConfigurationRoot Configuration { get; }
@ -46,15 +46,36 @@ namespace Ocelot.ManualTest
}; };
services.AddOcelotOutputCaching(settings); 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<string> {"admin", "openid", "offline_access"},
"Ocelot Administration",
true,
GrantTypes.ResourceOwnerPassword,
AccessTokenType.Jwt,
false,
new List<User>
{
new User("admin", username, hash, salt)
}
);
services.AddOcelot(Configuration, _identityServerConfig);
} }
public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{ {
loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddConsole(Configuration.GetSection("Logging"));
await app.UseOcelot(_identityServerConfig); await app.UseOcelot();
} }
} }
} }

View File

@ -14,7 +14,8 @@
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0", "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
"Microsoft.NETCore.App": "1.1.0", "Microsoft.NETCore.App": "1.1.0",
"Consul": "0.7.2.1", "Consul": "0.7.2.1",
"Polly": "5.0.3" "Polly": "5.0.3",
"Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0"
}, },
"tools": { "tools": {

View File

@ -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));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<IHashMatcher> _matcher;
private string _userName;
private string _password;
private ResourceOwnerPasswordValidationContext _context;
private Mock<IIdentityServerConfiguration> _config;
private User _user;
public OcelotResourceOwnerPasswordValidatorTests()
{
_matcher = new Mock<IHashMatcher>();
_config = new Mock<IIdentityServerConfiguration>();
_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>{_user});
}
private void GivenTheMatcherReturns(bool expected)
{
_matcher
.Setup(x => x.Match(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.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);
}
}
}

View File

@ -24,7 +24,8 @@
"Shouldly": "2.8.2", "Shouldly": "2.8.2",
"TestStack.BDDfy": "4.3.2", "TestStack.BDDfy": "4.3.2",
"Microsoft.AspNetCore.Authentication.OAuth": "1.1.0", "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": { "runtimes": {
"win10-x64": {}, "win10-x64": {},