Reorganized folder structure

This commit is contained in:
2024-09-26 10:20:30 +02:00
parent af7385678f
commit 27088f8217
92 changed files with 16 additions and 31 deletions

View File

@@ -0,0 +1,176 @@
using HopFrame.Api.Logic;
using HopFrame.Api.Models;
using HopFrame.Database;
using HopFrame.Database.Models.Entries;
using HopFrame.Security.Authentication;
using HopFrame.Security.Authorization;
using HopFrame.Security.Claims;
using HopFrame.Security.Models;
using HopFrame.Security.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace HopFrame.Api.Controller;
[ApiController]
[Route("authentication")]
public class SecurityController<TDbContext>(TDbContext context, IUserService users, ITokenContext tokenContext) : ControllerBase where TDbContext : HopDbContextBase {
[HttpPut("login")]
public async Task<ActionResult<SingleValueResult<string>>> Login([FromBody] UserLogin login) {
var user = await users.GetUserByEmail(login.Email);
if (user is null)
return LogicResult<SingleValueResult<string>>.NotFound("The provided email address was not found");
if (!await users.CheckUserPassword(user, login.Password))
return LogicResult<SingleValueResult<string>>.Forbidden("The provided password is not correct");
var refreshToken = new TokenEntry {
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.RefreshTokenType,
UserId = user.Id.ToString()
};
var accessToken = new TokenEntry {
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.AccessTokenType,
UserId = user.Id.ToString()
};
HttpContext.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
HttpContext.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
HttpOnly = true,
Secure = true
});
await context.Tokens.AddRangeAsync(refreshToken, accessToken);
await context.SaveChangesAsync();
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
}
[HttpPost("register")]
public async Task<ActionResult<SingleValueResult<string>>> Register([FromBody] UserRegister register) {
if (register.Password.Length < 8)
return LogicResult<SingleValueResult<string>>.Conflict("Password needs to be at least 8 characters long");
var allUsers = await users.GetUsers();
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
return LogicResult<SingleValueResult<string>>.Conflict("Username or Email is already registered");
var user = await users.AddUser(register);
var refreshToken = new TokenEntry {
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.RefreshTokenType,
UserId = user.Id.ToString()
};
var accessToken = new TokenEntry {
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.AccessTokenType,
UserId = user.Id.ToString()
};
await context.Tokens.AddRangeAsync(refreshToken, accessToken);
await context.SaveChangesAsync();
HttpContext.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
HttpContext.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
HttpOnly = false,
Secure = true
});
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
}
[HttpGet("authenticate")]
public async Task<ActionResult<SingleValueResult<string>>> Authenticate() {
var refreshToken = HttpContext.Request.Cookies[ITokenContext.RefreshTokenType];
if (string.IsNullOrEmpty(refreshToken))
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided");
var token = await context.Tokens.SingleOrDefaultAsync(token => token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType);
if (token is null)
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
if (token.CreatedAt + HopFrameAuthentication<TDbContext>.RefreshTokenTime < DateTime.Now)
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
var accessToken = new TokenEntry {
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.AccessTokenType,
UserId = token.UserId
};
await context.Tokens.AddAsync(accessToken);
await context.SaveChangesAsync();
HttpContext.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
HttpOnly = false,
Secure = true
});
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
}
[HttpDelete("logout"), Authorized]
public async Task<ActionResult> Logout() {
var accessToken = HttpContext.User.GetAccessTokenId();
var refreshToken = HttpContext.Request.Cookies[ITokenContext.RefreshTokenType];
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
return LogicResult.Conflict("access or refresh token not provided");
var tokenEntries = await context.Tokens.Where(token =>
(token.Token == accessToken && token.Type == TokenEntry.AccessTokenType) ||
(token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType))
.ToArrayAsync();
if (tokenEntries.Length != 2)
return LogicResult.NotFound("One or more of the provided tokens was not found");
context.Tokens.Remove(tokenEntries[0]);
context.Tokens.Remove(tokenEntries[1]);
await context.SaveChangesAsync();
HttpContext.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
HttpContext.Response.Cookies.Delete(ITokenContext.AccessTokenType);
return LogicResult.Ok();
}
[HttpDelete("delete"), Authorized]
public async Task<ActionResult> Delete([FromBody] UserPasswordValidation validation) {
var user = tokenContext.User;
if (!await users.CheckUserPassword(user, validation.Password))
return LogicResult.Forbidden("The provided password is not correct");
await users.DeleteUser(user);
HttpContext.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
HttpContext.Response.Cookies.Delete(ITokenContext.AccessTokenType);
return LogicResult.Ok();
}
}

View File

@@ -0,0 +1,86 @@
//Source: https://gist.github.com/damianh/5d69be0e3004024f03b6cc876d7b0bd3
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
using IMvcCoreBuilder = Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder;
namespace HopFrame.Api.Extensions;
public static class MvcExtensions {
/// <summary>
/// Finds the appropriate controllers
/// </summary>
/// <param name="partManager">The manager for the parts</param>
/// <param name="controllerTypes">The controller types that are allowed. </param>
public static void UseSpecificControllers(this ApplicationPartManager partManager, params Type[] controllerTypes) {
partManager.FeatureProviders.Add(new InternalControllerFeatureProvider());
//partManager.ApplicationParts.Clear();
partManager.ApplicationParts.Add(new SelectedControllersApplicationParts(controllerTypes));
}
/// <summary>
/// Only allow selected controllers
/// </summary>
/// <param name="mvcCoreBuilder">The builder that configures mvc core</param>
/// <param name="controllerTypes">The controller types that are allowed. </param>
public static IMvcCoreBuilder
UseSpecificControllers(this IMvcCoreBuilder mvcCoreBuilder, params Type[] controllerTypes) =>
mvcCoreBuilder.ConfigureApplicationPartManager(partManager =>
partManager.UseSpecificControllers(controllerTypes));
/// <summary>
/// Only instantiates selected controllers, not all of them. Prevents application scanning for controllers.
/// </summary>
private class SelectedControllersApplicationParts : ApplicationPart, IApplicationPartTypeProvider {
public SelectedControllersApplicationParts() {
Name = "Only allow selected controllers";
}
public SelectedControllersApplicationParts(Type[] types) {
Types = types.Select(x => x.GetTypeInfo()).ToArray();
}
public override string Name { get; }
public IEnumerable<TypeInfo> Types { get; }
}
/// <summary>
/// Ensure that internal controllers are also allowed. The default ControllerFeatureProvider hides internal controllers, but this one allows it.
/// </summary>
private class InternalControllerFeatureProvider : ControllerFeatureProvider {
private const string ControllerTypeNameSuffix = "Controller";
/// <summary>
/// Determines if a given <paramref name="typeInfo"/> is a controller. The default ControllerFeatureProvider hides internal controllers, but this one allows it.
/// </summary>
/// <param name="typeInfo">The <see cref="TypeInfo"/> candidate.</param>
/// <returns><code>true</code> if the type is a controller; otherwise <code>false</code>.</returns>
protected override bool IsController(TypeInfo typeInfo) {
if (!typeInfo.IsClass) {
return false;
}
if (typeInfo.IsAbstract) {
return false;
}
if (typeInfo.ContainsGenericParameters) {
return false;
}
if (typeInfo.IsDefined(typeof(Microsoft.AspNetCore.Mvc.NonControllerAttribute))) {
return false;
}
if (!typeInfo.Name.EndsWith(ControllerTypeNameSuffix, StringComparison.OrdinalIgnoreCase) &&
!typeInfo.IsDefined(typeof(Microsoft.AspNetCore.Mvc.ControllerAttribute))) {
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,20 @@
using HopFrame.Api.Controller;
using HopFrame.Database;
using HopFrame.Security.Authentication;
using Microsoft.Extensions.DependencyInjection;
namespace HopFrame.Api.Extensions;
public static class ServiceCollectionExtensions {
/// <summary>
/// Adds all HopFrame endpoints and the HopFrame security layer to the WebApplication
/// </summary>
/// <param name="services">The service provider to add the services to</param>
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
public static void AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
services.AddMvcCore().UseSpecificControllers(typeof(SecurityController<TDbContext>));
services.AddHopFrameAuthentication<TDbContext>();
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\HopFrame.Database\HopFrame.Database.csproj" />
<ProjectReference Include="..\HopFrame.Security\HopFrame.Security.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,21 @@
using System.Net;
namespace HopFrame.Api.Logic;
public interface ILogicResult {
HttpStatusCode State { get; set; }
string Message { get; set; }
bool IsSuccessful { get; }
}
public interface ILogicResult<T> {
HttpStatusCode State { get; set; }
T Data { get; set; }
string Message { get; set; }
bool IsSuccessful { get; }
}

View File

@@ -0,0 +1,189 @@
using System.Net;
using Microsoft.AspNetCore.Mvc;
namespace HopFrame.Api.Logic;
public class LogicResult : ILogicResult {
public HttpStatusCode State { get; set; }
public string Message { get; set; }
public bool IsSuccessful => State == HttpStatusCode.OK;
public static LogicResult Ok() {
return new LogicResult() {
State = HttpStatusCode.OK
};
}
public static LogicResult BadRequest() {
return new LogicResult() {
State = HttpStatusCode.BadRequest
};
}
public static LogicResult BadRequest(string message) {
return new LogicResult() {
State = HttpStatusCode.BadRequest,
Message = message
};
}
public static LogicResult Forbidden() {
return new LogicResult() {
State = HttpStatusCode.Forbidden
};
}
public static LogicResult Forbidden(string message) {
return new LogicResult() {
State = HttpStatusCode.Forbidden,
Message = message
};
}
public static LogicResult NotFound() {
return new LogicResult() {
State = HttpStatusCode.NotFound
};
}
public static LogicResult NotFound(string message) {
return new LogicResult() {
State = HttpStatusCode.NotFound,
Message = message
};
}
public static LogicResult Conflict() {
return new LogicResult() {
State = HttpStatusCode.Conflict
};
}
public static LogicResult Conflict(string message) {
return new LogicResult() {
State = HttpStatusCode.Conflict,
Message = message
};
}
public static LogicResult Forward(LogicResult result) {
return new LogicResult() {
State = result.State,
Message = result.Message
};
}
public static LogicResult Forward<T>(ILogicResult<T> result) {
return new LogicResult() {
State = result.State,
Message = result.Message
};
}
public static implicit operator ActionResult(LogicResult v) {
if (v.State == HttpStatusCode.OK) return new OkResult();
return new ObjectResult(v.Message) {
StatusCode = (int)v.State
};
}
}
public class LogicResult<T> : ILogicResult<T> {
public HttpStatusCode State { get; set; }
public T Data { get; set; }
public string Message { get; set; }
public bool IsSuccessful => State == HttpStatusCode.OK;
public static LogicResult<T> Ok() {
return new LogicResult<T>() {
State = HttpStatusCode.OK
};
}
public static LogicResult<T> Ok(T result) {
return new LogicResult<T>() {
State = HttpStatusCode.OK,
Data = result
};
}
public static LogicResult<T> BadRequest() {
return new LogicResult<T>() {
State = HttpStatusCode.BadRequest
};
}
public static LogicResult<T> BadRequest(string message) {
return new LogicResult<T>() {
State = HttpStatusCode.BadRequest,
Message = message
};
}
public static LogicResult<T> Forbidden() {
return new LogicResult<T>() {
State = HttpStatusCode.Forbidden
};
}
public static LogicResult<T> Forbidden(string message) {
return new LogicResult<T>() {
State = HttpStatusCode.Forbidden,
Message = message
};
}
public static LogicResult<T> NotFound() {
return new LogicResult<T>() {
State = HttpStatusCode.NotFound
};
}
public static LogicResult<T> NotFound(string message) {
return new LogicResult<T>() {
State = HttpStatusCode.NotFound,
Message = message
};
}
public static LogicResult<T> Conflict() {
return new LogicResult<T>() {
State = HttpStatusCode.Conflict
};
}
public static LogicResult<T> Conflict(string message) {
return new LogicResult<T>() {
State = HttpStatusCode.Conflict,
Message = message
};
}
public static LogicResult<T> Forward(ILogicResult result) {
return new LogicResult<T>() {
State = result.State,
Message = result.Message
};
}
public static LogicResult<T> Forward<T2>(ILogicResult<T2> result) {
return new LogicResult<T>() {
State = result.State,
Message = result.Message
};
}
public static implicit operator ActionResult<T>(LogicResult<T> v) {
if (v.State == HttpStatusCode.OK) return new OkObjectResult(v.Data);
return new ObjectResult(v.Message) {
StatusCode = (int)v.State
};
}
}

View File

@@ -0,0 +1,13 @@
namespace HopFrame.Api.Models;
public struct SingleValueResult<TValue>(TValue value) {
public TValue Value { get; set; } = value;
public static implicit operator TValue(SingleValueResult<TValue> v) {
return v.Value;
}
public static implicit operator SingleValueResult<TValue>(TValue v) {
return new SingleValueResult<TValue>(v);
}
}

View File

@@ -0,0 +1,5 @@
namespace HopFrame.Api.Models;
public sealed class UserPasswordValidation {
public string Password { get; set; }
}

View File

@@ -0,0 +1,30 @@
# HopFrame API module
This module contains some useful endpoints for user login / register management.
## Ho to use the Web API version
1. Add the HopFrame.Api library to your project:
```
dotnet add package HopFrame.Api
```
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
```csharp
public class DatabaseContext : HopDbContextBase {
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite("...");
}
}
```
3. Add the DbContext and HopFrame to your services
```csharp
builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddHopFrame<DatabaseContext>();
```