Provide mechanism to validate API keys. Re-organise structure to reflect new responsibility
This commit is contained in:
parent
87ae65316f
commit
9519bc623b
27 changed files with 245 additions and 36 deletions
30
src/Robware.Api.Auth.Tests/Controllers/ApiControllerTests.cs
Normal file
30
src/Robware.Api.Auth.Tests/Controllers/ApiControllerTests.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,10 +5,10 @@ using Microsoft.Extensions.Logging;
|
|||
using NSubstitute;
|
||||
using Robware.Api.Auth.Controllers;
|
||||
using Robware.Api.Auth.Models;
|
||||
using Robware.Auth;
|
||||
using Robware.Auth.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Robware.Api.Auth.Tests {
|
||||
namespace Robware.Api.Auth.Tests.Controllers {
|
||||
public class AuthControllerTests {
|
||||
private class TestUser : User {
|
||||
public TestUser(string username, string password) {
|
||||
|
@ -19,7 +19,7 @@ namespace Robware.Api.Auth.Tests {
|
|||
|
||||
[Fact]
|
||||
public async Task Authenticate_WithSuccessfulLoginRequest_ReturnsUser() {
|
||||
var logger = Substitute.For<ILogger<AuthController>>();
|
||||
var logger = Substitute.For<ILogger<UserController>>();
|
||||
var authenticator = Substitute.For<IAuthenticator>();
|
||||
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 controller = new AuthController(logger, authenticator);
|
||||
var controller = new UserController(logger, authenticator);
|
||||
(await controller.Authenticate(request)).Value.Should().BeEquivalentTo(expectation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Authenticate_WithIncorrectPassword_Returns401() {
|
||||
var logger = Substitute.For<ILogger<AuthController>>();
|
||||
var logger = Substitute.For<ILogger<UserController>>();
|
||||
var authenticator = Substitute.For<IAuthenticator>();
|
||||
authenticator.Authenticate("username", "password").Returns((AuthenticationResult.IncorrectPassword, null));
|
||||
|
||||
|
@ -45,13 +45,13 @@ namespace Robware.Api.Auth.Tests {
|
|||
Password = "password"
|
||||
};
|
||||
|
||||
var controller = new AuthController(logger, authenticator);
|
||||
var controller = new UserController(logger, authenticator);
|
||||
(await controller.Authenticate(request)).Result.Should().BeOfType<UnauthorizedResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Authenticate_WithIncorrectPassword_Returns404() {
|
||||
var logger = Substitute.For<ILogger<AuthController>>();
|
||||
var logger = Substitute.For<ILogger<UserController>>();
|
||||
var authenticator = Substitute.For<IAuthenticator>();
|
||||
authenticator.Authenticate("username", "password").Returns((AuthenticationResult.NotFound, null));
|
||||
|
||||
|
@ -60,7 +60,7 @@ namespace Robware.Api.Auth.Tests {
|
|||
Password = "password"
|
||||
};
|
||||
|
||||
var controller = new AuthController(logger, authenticator);
|
||||
var controller = new UserController(logger, authenticator);
|
||||
(await controller.Authenticate(request)).Result.Should().BeOfType<NotFoundResult>();
|
||||
}
|
||||
}
|
21
src/Robware.Api.Auth/Controllers/ApiController.cs
Normal file
21
src/Robware.Api.Auth/Controllers/ApiController.cs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -4,14 +4,16 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Robware.Api.Auth.Models;
|
||||
using Robware.Auth;
|
||||
using Robware.Auth.Users;
|
||||
|
||||
namespace Robware.Api.Auth.Controllers {
|
||||
[ApiController]
|
||||
public class AuthController : ControllerBase {
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
[Route("[controller]")]
|
||||
public class UserController : ControllerBase {
|
||||
private readonly ILogger<UserController> _logger;
|
||||
private readonly IAuthenticator _authenticator;
|
||||
|
||||
public AuthController(ILogger<AuthController> logger, IAuthenticator authenticator) {
|
||||
public UserController(ILogger<UserController> logger, IAuthenticator authenticator) {
|
||||
_logger = logger;
|
||||
_authenticator = authenticator;
|
||||
}
|
|
@ -3,8 +3,10 @@ using Microsoft.AspNetCore.Hosting;
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Robware.Auth;
|
||||
using Robware.Data;
|
||||
using Robware.Auth.API;
|
||||
using Robware.Auth.Users;
|
||||
using Robware.Data.API;
|
||||
using Robware.Data.Users;
|
||||
|
||||
namespace Robware.Api.Auth {
|
||||
public class Startup {
|
||||
|
@ -21,7 +23,9 @@ namespace Robware.Api.Auth {
|
|||
services.AddSingleton<ICryptographyProvider, CryptographyProvider>()
|
||||
.AddSingleton<IAuthenticator, Authenticator>()
|
||||
.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.
|
||||
|
|
52
src/Robware.Auth.Tests/API/ApiKeyValidatorTests.cs
Normal file
52
src/Robware.Auth.Tests/API/ApiKeyValidatorTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,10 +2,10 @@
|
|||
using FluentAssertions;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Robware.Data;
|
||||
using Robware.Auth.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Robware.Auth.Tests {
|
||||
namespace Robware.Auth.Tests.Users {
|
||||
public class AuthenticatorTests {
|
||||
[Fact]
|
||||
public async Task Authenticate_ForUserThatExistsWithCorrectPassword_ReturnsOkResultWithUser() {
|
|
@ -1,7 +1,8 @@
|
|||
using FluentAssertions;
|
||||
using Robware.Auth.Users;
|
||||
using Xunit;
|
||||
|
||||
namespace Robware.Auth.Tests {
|
||||
namespace Robware.Auth.Tests.Users {
|
||||
public class CryoptographyProviderTests {
|
||||
[Fact]
|
||||
public void Encrypt_WithInput_ReturnsHash() {
|
|
@ -1,4 +1,6 @@
|
|||
namespace Robware.Auth.Tests {
|
||||
using Robware.Auth.Users;
|
||||
|
||||
namespace Robware.Auth.Tests.Users {
|
||||
internal class TestUser : User {
|
||||
public TestUser(string username, string password) {
|
||||
Username = username;
|
10
src/Robware.Auth/API/ApiKey.cs
Normal file
10
src/Robware.Auth/API/ApiKey.cs
Normal 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; }
|
||||
}
|
||||
}
|
9
src/Robware.Auth/API/ApiKeyNotFoundException.cs
Normal file
9
src/Robware.Auth/API/ApiKeyNotFoundException.cs
Normal 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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
22
src/Robware.Auth/API/ApiKeyValidator.cs
Normal file
22
src/Robware.Auth/API/ApiKeyValidator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
src/Robware.Auth/API/IApiKeyValidator.cs
Normal file
7
src/Robware.Auth/API/IApiKeyValidator.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace Robware.Auth.API {
|
||||
public interface IApiKeyValidator {
|
||||
Task<bool> Validate(string key);
|
||||
}
|
||||
}
|
7
src/Robware.Auth/API/IApiKeys.cs
Normal file
7
src/Robware.Auth/API/IApiKeys.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace Robware.Auth.API {
|
||||
public interface IApiKeys {
|
||||
Task<ApiKey> Get(string key);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
private readonly IUsers _users;
|
||||
private readonly ICryptographyProvider _crypto;
|
|
@ -1,7 +1,7 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Robware.Auth {
|
||||
namespace Robware.Auth.Users {
|
||||
public class CryptographyProvider : ICryptographyProvider {
|
||||
public string Encrypt(string input) {
|
||||
using (var sha256 = SHA256.Create()) {
|
|
@ -1,6 +1,6 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace Robware.Auth {
|
||||
namespace Robware.Auth.Users {
|
||||
public interface IAuthenticator {
|
||||
Task<(AuthenticationResult Result, User User)> Authenticate(string username, string password);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
namespace Robware.Auth {
|
||||
namespace Robware.Auth.Users {
|
||||
public interface ICryptographyProvider {
|
||||
string Encrypt(string input);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace Robware.Auth {
|
||||
namespace Robware.Auth.Users {
|
||||
public interface IUsers {
|
||||
Task<User> GetByEmail(string email);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
namespace Robware.Auth {
|
||||
namespace Robware.Auth.Users {
|
||||
public class User {
|
||||
public string Username { get; protected set; }
|
||||
public string Password { get; protected set; }
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
|
||||
namespace Robware.Auth {
|
||||
namespace Robware.Auth.Users {
|
||||
public class UserNotFoundException : Exception {
|
||||
public UserNotFoundException(string username) : base("Could not find user " + username) {
|
||||
|
43
src/Robware.Data/API/ApiKeyRepository.cs
Normal file
43
src/Robware.Data/API/ApiKeyRepository.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
using Robware.Auth;
|
||||
using Robware.Data.States;
|
||||
using Robware.Auth.Users;
|
||||
using Robware.Data.Users.States;
|
||||
|
||||
namespace Robware.Data {
|
||||
namespace Robware.Data.Users {
|
||||
public class DatabaseUser : User {
|
||||
public DatabaseUser(UserState state) {
|
||||
Username = state.User_Email;
|
|
@ -1,6 +1,6 @@
|
|||
using System.Data;
|
||||
|
||||
namespace Robware.Data {
|
||||
namespace Robware.Data.Users {
|
||||
public interface IDatabaseProvider {
|
||||
IDbConnection NewConnection();
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
using System.Data;
|
||||
using MySql.Data.MySqlClient;
|
||||
|
||||
namespace Robware.Data {
|
||||
namespace Robware.Data.Users {
|
||||
public class MySQLDatabaseProvider : IDatabaseProvider {
|
||||
private readonly string _connectionString;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Robware.Data.States {
|
||||
namespace Robware.Data.Users.States {
|
||||
public class UserState {
|
||||
public string User_Id { get; set; }
|
||||
public string User_Email { get; set; }
|
|
@ -1,10 +1,10 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using Robware.Auth;
|
||||
using Robware.Data.States;
|
||||
using Robware.Auth.Users;
|
||||
using Robware.Data.Users.States;
|
||||
|
||||
namespace Robware.Data {
|
||||
namespace Robware.Data.Users {
|
||||
public class UserRepository : IUsers {
|
||||
private readonly IDatabaseProvider _dbProvider;
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue