Provide mechanism to validate API keys. Re-organise structure to reflect new responsibility

This commit is contained in:
Robert Marshall 2020-04-19 18:16:42 +01:00
parent 87ae65316f
commit 9519bc623b
27 changed files with 245 additions and 36 deletions

View file

@ -0,0 +1,30 @@
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Robware.Api.Auth.Controllers;
using Robware.Auth.API;
using Xunit;
namespace Robware.Api.Auth.Tests.Controllers {
public class ApiControllerTests {
[Fact]
public async Task Validate_WithValidKey_ReturnsOk() {
var logger = Substitute.For<ILogger<ApiController>>();
var apiKeyValidator = Substitute.For<IApiKeyValidator>();
apiKeyValidator.Validate("key").Returns(true);
var controller = new ApiController(logger, apiKeyValidator);
(await controller.Validate("key")).Should().BeOfType<OkResult>();
}
[Fact]
public async Task Validate_WithInvalidKey_ReturnsUnauthorised() {
var logger = Substitute.For<ILogger<ApiController>>();
var apiKeyValidator = Substitute.For<IApiKeyValidator>();
apiKeyValidator.Validate("key").Returns(false);
var controller = new ApiController(logger, apiKeyValidator);
(await controller.Validate("key")).Should().BeOfType<UnauthorizedResult>();
}
}
}

View file

@ -5,10 +5,10 @@ using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using Robware.Api.Auth.Controllers; using Robware.Api.Auth.Controllers;
using Robware.Api.Auth.Models; using Robware.Api.Auth.Models;
using Robware.Auth; using Robware.Auth.Users;
using Xunit; using Xunit;
namespace Robware.Api.Auth.Tests { namespace Robware.Api.Auth.Tests.Controllers {
public class AuthControllerTests { public class AuthControllerTests {
private class TestUser : User { private class TestUser : User {
public TestUser(string username, string password) { public TestUser(string username, string password) {
@ -19,7 +19,7 @@ namespace Robware.Api.Auth.Tests {
[Fact] [Fact]
public async Task Authenticate_WithSuccessfulLoginRequest_ReturnsUser() { public async Task Authenticate_WithSuccessfulLoginRequest_ReturnsUser() {
var logger = Substitute.For<ILogger<AuthController>>(); var logger = Substitute.For<ILogger<UserController>>();
var authenticator = Substitute.For<IAuthenticator>(); var authenticator = Substitute.For<IAuthenticator>();
authenticator.Authenticate("username", "password").Returns((AuthenticationResult.Success, new TestUser("username", "password"))); authenticator.Authenticate("username", "password").Returns((AuthenticationResult.Success, new TestUser("username", "password")));
@ -30,13 +30,13 @@ namespace Robware.Api.Auth.Tests {
var expectation = new TestUser("username", "password"); var expectation = new TestUser("username", "password");
var controller = new AuthController(logger, authenticator); var controller = new UserController(logger, authenticator);
(await controller.Authenticate(request)).Value.Should().BeEquivalentTo(expectation); (await controller.Authenticate(request)).Value.Should().BeEquivalentTo(expectation);
} }
[Fact] [Fact]
public async Task Authenticate_WithIncorrectPassword_Returns401() { public async Task Authenticate_WithIncorrectPassword_Returns401() {
var logger = Substitute.For<ILogger<AuthController>>(); var logger = Substitute.For<ILogger<UserController>>();
var authenticator = Substitute.For<IAuthenticator>(); var authenticator = Substitute.For<IAuthenticator>();
authenticator.Authenticate("username", "password").Returns((AuthenticationResult.IncorrectPassword, null)); authenticator.Authenticate("username", "password").Returns((AuthenticationResult.IncorrectPassword, null));
@ -45,13 +45,13 @@ namespace Robware.Api.Auth.Tests {
Password = "password" Password = "password"
}; };
var controller = new AuthController(logger, authenticator); var controller = new UserController(logger, authenticator);
(await controller.Authenticate(request)).Result.Should().BeOfType<UnauthorizedResult>(); (await controller.Authenticate(request)).Result.Should().BeOfType<UnauthorizedResult>();
} }
[Fact] [Fact]
public async Task Authenticate_WithIncorrectPassword_Returns404() { public async Task Authenticate_WithIncorrectPassword_Returns404() {
var logger = Substitute.For<ILogger<AuthController>>(); var logger = Substitute.For<ILogger<UserController>>();
var authenticator = Substitute.For<IAuthenticator>(); var authenticator = Substitute.For<IAuthenticator>();
authenticator.Authenticate("username", "password").Returns((AuthenticationResult.NotFound, null)); authenticator.Authenticate("username", "password").Returns((AuthenticationResult.NotFound, null));
@ -60,7 +60,7 @@ namespace Robware.Api.Auth.Tests {
Password = "password" Password = "password"
}; };
var controller = new AuthController(logger, authenticator); var controller = new UserController(logger, authenticator);
(await controller.Authenticate(request)).Result.Should().BeOfType<NotFoundResult>(); (await controller.Authenticate(request)).Result.Should().BeOfType<NotFoundResult>();
} }
} }

View file

@ -0,0 +1,21 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Robware.Auth.API;
namespace Robware.Api.Auth.Controllers {
[ApiController]
[Route("[controller]")]
public class ApiController : ControllerBase {
private readonly ILogger<ApiController> _logger;
private readonly IApiKeyValidator _apiKeyValidator;
public ApiController(ILogger<ApiController> logger, IApiKeyValidator apiKeyValidator) {
_logger = logger;
_apiKeyValidator = apiKeyValidator;
}
[HttpGet(nameof(Validate))]
public async Task<ActionResult> Validate(string key) => await _apiKeyValidator.Validate(key) ? (ActionResult) Ok() : Unauthorized();
}
}

View file

@ -4,14 +4,16 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Robware.Api.Auth.Models; using Robware.Api.Auth.Models;
using Robware.Auth; using Robware.Auth;
using Robware.Auth.Users;
namespace Robware.Api.Auth.Controllers { namespace Robware.Api.Auth.Controllers {
[ApiController] [ApiController]
public class AuthController : ControllerBase { [Route("[controller]")]
private readonly ILogger<AuthController> _logger; public class UserController : ControllerBase {
private readonly ILogger<UserController> _logger;
private readonly IAuthenticator _authenticator; private readonly IAuthenticator _authenticator;
public AuthController(ILogger<AuthController> logger, IAuthenticator authenticator) { public UserController(ILogger<UserController> logger, IAuthenticator authenticator) {
_logger = logger; _logger = logger;
_authenticator = authenticator; _authenticator = authenticator;
} }

View file

@ -3,8 +3,10 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Robware.Auth; using Robware.Auth.API;
using Robware.Data; using Robware.Auth.Users;
using Robware.Data.API;
using Robware.Data.Users;
namespace Robware.Api.Auth { namespace Robware.Api.Auth {
public class Startup { public class Startup {
@ -21,7 +23,9 @@ namespace Robware.Api.Auth {
services.AddSingleton<ICryptographyProvider, CryptographyProvider>() services.AddSingleton<ICryptographyProvider, CryptographyProvider>()
.AddSingleton<IAuthenticator, Authenticator>() .AddSingleton<IAuthenticator, Authenticator>()
.AddSingleton<IDatabaseProvider>(new MySQLDatabaseProvider(Configuration.GetConnectionString("database"))) .AddSingleton<IDatabaseProvider>(new MySQLDatabaseProvider(Configuration.GetConnectionString("database")))
.AddSingleton<IUsers, UserRepository>(); .AddSingleton<IUsers, UserRepository>()
.AddSingleton<IApiKeyValidator, ApiKeyValidator>()
.AddSingleton<IApiKeys, ApiKeyRepository>();
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View file

@ -0,0 +1,52 @@
using System;
using System.Threading.Tasks;
using FluentAssertions;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Robware.Auth.API;
using Xunit;
namespace Robware.Auth.Tests.API {
public class ApiKeyValidatorTests {
[Fact]
public async Task Validate_WithKeyThatExistsAndIsEnabled_ReturnsTrue() {
var apiKey = new ApiKey {
Name = "Test Key",
IssueTimestamp = new DateTime(2020, 4, 19),
Key = "3c6e0b8a9c15224a8228b9a98ca1531d",
Enabled = true
};
var keys = Substitute.For<IApiKeys>();
keys.Get("3c6e0b8a9c15224a8228b9a98ca1531d").Returns(apiKey);
var validator = new ApiKeyValidator(keys);
(await validator.Validate("3c6e0b8a9c15224a8228b9a98ca1531d")).Should().BeTrue();
}
[Fact]
public async Task Validate_WithKeyThatExistsAndIsDisabled_ReturnsFalse() {
var apiKey = new ApiKey {
Name = "Test Key",
IssueTimestamp = new DateTime(2020, 4, 19),
Key = "3c6e0b8a9c15224a8228b9a98ca1531d",
Enabled = false
};
var keys = Substitute.For<IApiKeys>();
keys.Get("3c6e0b8a9c15224a8228b9a98ca1531d").Returns(apiKey);
var validator = new ApiKeyValidator(keys);
(await validator.Validate("3c6e0b8a9c15224a8228b9a98ca1531d")).Should().BeFalse();
}
[Fact]
public async Task Validate_WithKeyThatDoesntExist_ReturnsFalse() {
var keys = Substitute.For<IApiKeys>();
keys.Get("3c6e0b8a9c15224a8228b9a98ca1531d").Throws(new ApiKeyNotFoundException(""));
var validator = new ApiKeyValidator(keys);
(await validator.Validate("3c6e0b8a9c15224a8228b9a98ca1531d")).Should().BeFalse();
}
}
}

View file

@ -2,10 +2,10 @@
using FluentAssertions; using FluentAssertions;
using NSubstitute; using NSubstitute;
using NSubstitute.ExceptionExtensions; using NSubstitute.ExceptionExtensions;
using Robware.Data; using Robware.Auth.Users;
using Xunit; using Xunit;
namespace Robware.Auth.Tests { namespace Robware.Auth.Tests.Users {
public class AuthenticatorTests { public class AuthenticatorTests {
[Fact] [Fact]
public async Task Authenticate_ForUserThatExistsWithCorrectPassword_ReturnsOkResultWithUser() { public async Task Authenticate_ForUserThatExistsWithCorrectPassword_ReturnsOkResultWithUser() {

View file

@ -1,7 +1,8 @@
using FluentAssertions; using FluentAssertions;
using Robware.Auth.Users;
using Xunit; using Xunit;
namespace Robware.Auth.Tests { namespace Robware.Auth.Tests.Users {
public class CryoptographyProviderTests { public class CryoptographyProviderTests {
[Fact] [Fact]
public void Encrypt_WithInput_ReturnsHash() { public void Encrypt_WithInput_ReturnsHash() {

View file

@ -1,4 +1,6 @@
namespace Robware.Auth.Tests { using Robware.Auth.Users;
namespace Robware.Auth.Tests.Users {
internal class TestUser : User { internal class TestUser : User {
public TestUser(string username, string password) { public TestUser(string username, string password) {
Username = username; Username = username;

View file

@ -0,0 +1,10 @@
using System;
namespace Robware.Auth.API {
public class ApiKey {
public string Name { get; set; }
public DateTime IssueTimestamp { get; set; }
public string Key { get; set; }
public bool Enabled { get; set; }
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace Robware.Auth.API {
public class ApiKeyNotFoundException :Exception {
public ApiKeyNotFoundException(string key) : base("Could not find API key " + key) {
}
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Threading.Tasks;
namespace Robware.Auth.API {
public class ApiKeyValidator : IApiKeyValidator {
private readonly IApiKeys _apiKeys;
public ApiKeyValidator(IApiKeys apiKeys) {
_apiKeys = apiKeys;
}
public async Task<bool> Validate(string key) {
try {
var apiKey = await _apiKeys.Get(key);
return apiKey.Enabled;
}
catch (ApiKeyNotFoundException) {
return false;
}
}
}
}

View file

@ -0,0 +1,7 @@
using System.Threading.Tasks;
namespace Robware.Auth.API {
public interface IApiKeyValidator {
Task<bool> Validate(string key);
}
}

View file

@ -0,0 +1,7 @@
using System.Threading.Tasks;
namespace Robware.Auth.API {
public interface IApiKeys {
Task<ApiKey> Get(string key);
}
}

View file

@ -1,7 +1,6 @@
using System; using System.Threading.Tasks;
using System.Threading.Tasks;
namespace Robware.Auth { namespace Robware.Auth.Users {
public class Authenticator : IAuthenticator { public class Authenticator : IAuthenticator {
private readonly IUsers _users; private readonly IUsers _users;
private readonly ICryptographyProvider _crypto; private readonly ICryptographyProvider _crypto;

View file

@ -1,7 +1,7 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
namespace Robware.Auth { namespace Robware.Auth.Users {
public class CryptographyProvider : ICryptographyProvider { public class CryptographyProvider : ICryptographyProvider {
public string Encrypt(string input) { public string Encrypt(string input) {
using (var sha256 = SHA256.Create()) { using (var sha256 = SHA256.Create()) {

View file

@ -1,6 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Robware.Auth { namespace Robware.Auth.Users {
public interface IAuthenticator { public interface IAuthenticator {
Task<(AuthenticationResult Result, User User)> Authenticate(string username, string password); Task<(AuthenticationResult Result, User User)> Authenticate(string username, string password);
} }

View file

@ -1,4 +1,4 @@
namespace Robware.Auth { namespace Robware.Auth.Users {
public interface ICryptographyProvider { public interface ICryptographyProvider {
string Encrypt(string input); string Encrypt(string input);
} }

View file

@ -1,6 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Robware.Auth { namespace Robware.Auth.Users {
public interface IUsers { public interface IUsers {
Task<User> GetByEmail(string email); Task<User> GetByEmail(string email);
} }

View file

@ -1,4 +1,4 @@
namespace Robware.Auth { namespace Robware.Auth.Users {
public class User { public class User {
public string Username { get; protected set; } public string Username { get; protected set; }
public string Password { get; protected set; } public string Password { get; protected set; }

View file

@ -1,6 +1,6 @@
using System; using System;
namespace Robware.Auth { namespace Robware.Auth.Users {
public class UserNotFoundException : Exception { public class UserNotFoundException : Exception {
public UserNotFoundException(string username) : base("Could not find user " + username) { public UserNotFoundException(string username) : base("Could not find user " + username) {

View file

@ -0,0 +1,43 @@
using System;
using System.Threading.Tasks;
using System.Linq;
using Dapper;
using Robware.Auth.API;
using Robware.Data.Users;
namespace Robware.Data.API {
public class ApiKeyRepository : IApiKeys {
private readonly IDatabaseProvider _dbProvider;
public ApiKeyRepository(IDatabaseProvider dbProvider) {
_dbProvider = dbProvider;
}
public async Task<ApiKey> Get(string key) {
const string query = "SELECT * FROM api_keys WHERE api_key=@key";
using (var connection = _dbProvider.NewConnection()) {
connection.Open();
var result = await connection.QueryAsync<ApiKeyState>(query, new {key});
if (!result.Any())
throw new ApiKeyNotFoundException(key);
var dbKey = result.Single();
return new ApiKey {
Key = dbKey.Api_Key,
Enabled = dbKey.Enabled,
IssueTimestamp = dbKey.IssueTimestamp,
Name = dbKey.Name
};
}
}
}
public class ApiKeyState {
public string Name { get; set; }
public DateTime IssueTimestamp { get; set; }
public string Api_Key { get; set; }
public bool Enabled { get; set; }
}
}

View file

@ -1,7 +1,7 @@
using Robware.Auth; using Robware.Auth.Users;
using Robware.Data.States; using Robware.Data.Users.States;
namespace Robware.Data { namespace Robware.Data.Users {
public class DatabaseUser : User { public class DatabaseUser : User {
public DatabaseUser(UserState state) { public DatabaseUser(UserState state) {
Username = state.User_Email; Username = state.User_Email;

View file

@ -1,6 +1,6 @@
using System.Data; using System.Data;
namespace Robware.Data { namespace Robware.Data.Users {
public interface IDatabaseProvider { public interface IDatabaseProvider {
IDbConnection NewConnection(); IDbConnection NewConnection();
} }

View file

@ -1,7 +1,7 @@
using System.Data; using System.Data;
using MySql.Data.MySqlClient; using MySql.Data.MySqlClient;
namespace Robware.Data { namespace Robware.Data.Users {
public class MySQLDatabaseProvider : IDatabaseProvider { public class MySQLDatabaseProvider : IDatabaseProvider {
private readonly string _connectionString; private readonly string _connectionString;

View file

@ -1,4 +1,4 @@
namespace Robware.Data.States { namespace Robware.Data.Users.States {
public class UserState { public class UserState {
public string User_Id { get; set; } public string User_Id { get; set; }
public string User_Email { get; set; } public string User_Email { get; set; }

View file

@ -1,10 +1,10 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper; using Dapper;
using Robware.Auth; using Robware.Auth.Users;
using Robware.Data.States; using Robware.Data.Users.States;
namespace Robware.Data { namespace Robware.Data.Users {
public class UserRepository : IUsers { public class UserRepository : IUsers {
private readonly IDatabaseProvider _dbProvider; private readonly IDatabaseProvider _dbProvider;