Archived
Private
Public Access
1
0

Initial commit

This commit is contained in:
2022-09-04 12:03:44 +02:00
commit 15f48d259f
91 changed files with 22716 additions and 0 deletions

25
Backend/.dockerignore Normal file
View File

@@ -0,0 +1,25 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

2
Backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
obj
bin

14
Backend/Backend.csproj Normal file
View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MySql.EntityFrameworkCore" Version="6.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Mvc;
namespace Backend.Controllers;
[ApiController]
public class TestController : ControllerBase {
private DatabaseContext _context;
public TestController(DatabaseContext context) {
_context = context;
}
[HttpGet("")]
public IActionResult InitializeDb() {
_context.ExecuteTableCreation();
return Ok("OK");
}
}

View File

@@ -0,0 +1,98 @@
using Backend.Entitys;
using Backend.Logic;
using Backend.LogicResults;
using Backend.Security;
using Backend.Security.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Backend.Controllers;
[ApiController]
[Route("users")]
public class UserController : ControllerBase {
private readonly UserLogic _logic;
private readonly ITokenContext _context;
public UserController(UserLogic logic, ITokenContext context) {
_logic = logic;
_context = context;
}
[HttpGet]
[Authorized(Permissions.ShowUsers)]
public ActionResult<IEnumerable<User>> GetUsers() {
return this.FromLogicResult(_logic.GetUsers());
}
[HttpPut("login")]
public ActionResult<AccessToken> Login([FromBody] UserLogin login) {
return this.FromLogicResult(_logic.Login(login));
}
[HttpPost("register")]
public ActionResult<AccessToken> Register([FromBody] UserEditor editor) {
return this.FromLogicResult(_logic.Register(editor));
}
[HttpGet("token")]
[Authorized]
public ActionResult<AccessToken> GetToken() {
return this.FromLogicResult(_logic.GenerateToken(_logic.GetCurrentUserRefreshToken()));
}
[HttpGet("{userId}")]
[Authorized(Permissions.ShowUsers)]
public ActionResult<User> GetUser(Guid userId) {
return this.FromLogicResult(_logic.GetUser(userId));
}
[HttpGet("self")]
[Authorized]
public ActionResult<User> GetOwnUser() {
return this.FromLogicResult(_logic.GetUser(_context.UserId));
}
[HttpPut("{userId}")]
[Authorized(Permissions.EditUsers)]
public ActionResult EditUser(Guid userId, [FromBody] UserEditor editor) {
return this.FromLogicResult(_logic.EditUser(userId, editor));
}
[HttpDelete("{userId}")]
[Authorized(Permissions.DeleteUsers)]
public ActionResult DeleteUser(Guid userId) {
return this.FromLogicResult(_logic.DeleteUser(userId));
}
[HttpGet("{userId}/permissions")]
[Authorized(Permissions.ShowUserPermissions)]
public ActionResult<IEnumerable<string>> GetUserPermissions(Guid userId) {
return this.FromLogicResult(_logic.GetPermissions(userId));
}
[HttpGet("{userId}/permissions/raw")]
[Authorized(Permissions.ShowUserPermissions)]
public ActionResult<IEnumerable<string>> GetUserPermissionsRaw(Guid userId) {
return this.FromLogicResult(_logic.GetPermissionsRaw(userId));
}
[HttpPost("{userId}/permissions")]
[Authorized(Permissions.EditUserPermissions, Permissions.EditOwnPermissions)]
public ActionResult AddUserPermissions(Guid userId, [FromBody] string[] permissions) {
return this.FromLogicResult(_logic.AddPermissions(userId, permissions));
}
[HttpPut("{userId}/permissions")]
[Authorized(Permissions.EditUserPermissions, Permissions.EditOwnPermissions)]
public ActionResult DeleteUserPermissions(Guid userId, [FromBody] string[] permissions) {
return this.FromLogicResult(_logic.DeletePermissions(userId, permissions));
}
[HttpDelete("{userId}/logout")]
[Authorized(Permissions.LogoutUsers)]
public ActionResult Logout(Guid userId) {
return this.FromLogicResult(_logic.Logout(userId));
}
}

View File

@@ -0,0 +1,65 @@
using Backend.Entitys;
using Microsoft.EntityFrameworkCore;
namespace Backend;
public class DatabaseContext : DbContext {
private string _connectionString;
public DbSet<User> Users { get; set; }
public DbSet<RefreshToken> RefreshTokens { get; set; }
public DbSet<AccessToken> AccessTokens { get; set; }
public DbSet<Permission> Permissions { get; set; }
public DatabaseContext(IConfiguration configuration) {
_connectionString = configuration.GetSection("MySQL").Get<string>();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
if (string.IsNullOrEmpty(_connectionString))
throw new ArgumentException("MySQL Connection String was not defined correctly in the Configuration!");
optionsBuilder.UseMySQL(_connectionString);
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>(entry => {
entry.HasKey(e => e.Id);
entry.Property(e => e.FirstName);
entry.Property(e => e.LastName);
entry.Property(e => e.Email);
entry.Property(e => e.Username);
entry.Property(e => e.Password);
entry.Property(e => e.Created);
});
modelBuilder.Entity<RefreshToken>(entry => {
entry.HasKey(e => e.Id);
entry.Property(e => e.UserId);
entry.Property(e => e.ExpirationDate);
});
modelBuilder.Entity<AccessToken>(entry => {
entry.HasKey(e => e.Id);
entry.Property(e => e.RefreshTokenId);
entry.Property(e => e.ExpirationDate);
});
modelBuilder.Entity<Permission>(entry => {
entry.HasKey(e => e.Id);
entry.Property(e => e.Id).ValueGeneratedOnAdd();
entry.Property(e => e.UserId);
entry.Property(e => e.PermissionKey);
});
}
public void ExecuteTableCreation() {
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Users (Id VARCHAR(50) PRIMARY KEY, FirstName VARCHAR(255), LastName VARCHAR(255), Email VARCHAR(255), Username VARCHAR(255), Password VARCHAR(255), Created TIMESTAMP)");
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS RefreshTokens (Id VARCHAR(50) PRIMARY KEY, UserId VARCHAR(50), ExpirationDate TIMESTAMP)");
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS AccessTokens (Id VARCHAR(50) PRIMARY KEY, RefreshTokenId VARCHAR(50), ExpirationDate TIMESTAMP)");
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Permissions (Id INT PRIMARY KEY AUTO_INCREMENT, UserId VARCHAR(50), PermissionName VARCHAR(100))");
}
}

20
Backend/Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["Backend.csproj", ""]
RUN dotnet restore "Backend.csproj"
COPY . .
WORKDIR "/src"
RUN dotnet build "Backend.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Backend.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Backend.dll"]

View File

@@ -0,0 +1,16 @@
using System;
namespace Backend.Entitys;
public class Permission {
public int Id { get; set; }
public Guid UserId { get; set; }
public string PermissionKey { get; set; }
}
public class PermissionGroup {
public string Permission { get; set; }
public string Name { get; set; }
public string[] Permissions { get; set; }
public string[] Inherits { get; set; }
}

20
Backend/Entitys/Tokens.cs Normal file
View File

@@ -0,0 +1,20 @@
using System;
namespace Backend.Entitys;
public class Tokens {
public AccessToken AccessToken { get; set; }
public RefreshToken RefreshToken { get; set; }
}
public class RefreshToken {
public Guid Id { get; set; }
public Guid UserId { get; set; }
public DateTime ExpirationDate { get; set; }
}
public class AccessToken {
public Guid Id { get; set; }
public Guid RefreshTokenId { get; set; }
public DateTime ExpirationDate { get; set; }
}

21
Backend/Entitys/User.cs Normal file
View File

@@ -0,0 +1,21 @@
using System;
namespace Backend.Entitys;
public class User : UserEditor {
public Guid Id { get; set; }
public DateTime Created { get; set; }
}
public class UserLogin {
public string UsernameOrEmail { get; set; }
public string Password { get; set; }
}
public class UserEditor {
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}

213
Backend/Logic/UserLogic.cs Normal file
View File

@@ -0,0 +1,213 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Backend.Entitys;
using Backend.LogicResults;
using Backend.Options;
using Backend.Repositorys;
using Backend.Security;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace Backend.Logic;
public class UserLogic {
private UserRepository _users;
private TokenRepository _tokens;
private GroupRepository _groups;
private UserMessageOptions _messages;
private IHttpContextAccessor _contextAccessor;
private ITokenContext _context;
public UserLogic(
UserRepository users,
TokenRepository tokens,
GroupRepository groups,
IOptions<UserMessageOptions> messages,
IHttpContextAccessor contextAccessor,
ITokenContext context) {
_users = users;
_tokens = tokens;
_groups = groups;
_messages = messages.Value;
_contextAccessor = contextAccessor;
_context = context;
}
public ILogicResult<IEnumerable<User>> GetUsers() {
var users = _users.GetUsers()
.Select(user => new User {
Id = user.Id,
Created = user.Created,
Email = user.Email,
FirstName = user.FirstName,
LastName = user.LastName,
Username = user.Username
});
return LogicResult<IEnumerable<User>>.Ok(users);
}
public ILogicResult<User> GetUser(Guid userId) {
var user = _users.GetUser(userId);
if (user is null) return LogicResult<User>.NotFound(_messages.NotFound);
return LogicResult<User>.Ok(new User {
Id = user.Id,
Created = user.Created,
Email = user.Email,
FirstName = user.FirstName,
LastName = user.LastName,
Username = user.Username
});
}
public ILogicResult EditUser(Guid userId, UserEditor editor) {
if (!ValidateEdit(editor)) return LogicResult.BadRequest(_messages.InvalidEditData);
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
_users.EditUser(userId, editor);
return LogicResult.Ok();
}
public ILogicResult DeleteUser(Guid userId) {
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
_tokens.DeleteUserTokens(userId);
_users.DeleteUser(userId);
return LogicResult.Ok();
}
public ILogicResult<IEnumerable<string>> GetPermissions(Guid userId) {
if (_users.GetUser(userId) is null) return LogicResult<IEnumerable<string>>.NotFound(_messages.NotFound);
return LogicResult<IEnumerable<string>>.Ok(_groups.GetUserPermissions(userId).Select(perm => perm.PermissionKey));
}
public ILogicResult<IEnumerable<string>> GetPermissionsRaw(Guid userId) {
if (_users.GetUser(userId) is null) return LogicResult<IEnumerable<string>>.NotFound(_messages.NotFound);
return LogicResult<IEnumerable<string>>.Ok(_groups.GetUserPermissionsRaw(userId).Select(perm => perm.PermissionKey));
}
public ILogicResult AddPermissions(Guid userId, params string[] permissions) {
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
_groups.AddPermissions(userId, permissions);
return LogicResult.Ok();
}
public ILogicResult DeletePermissions(Guid userId, params string[] permissions) {
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
_groups.DeletePermissions(userId, permissions);
return LogicResult.Ok();
}
public ILogicResult<AccessToken> Login(UserLogin login) {
var user = _users.GetUsers().SingleOrDefault(user =>
user.Username == login.UsernameOrEmail || user.Email == login.UsernameOrEmail);
if (user is null) return LogicResult<AccessToken>.NotFound(_messages.NotFound);
if (user.Password != TokenRepository.Hash128(login.Password, user.Email))
return LogicResult<AccessToken>.BadRequest(_messages.WrongPassword);
_tokens.DeleteUserTokens(user.Id);
var refreshToken = _tokens.CreateRefreshToken(user.Id);
var accessToken = _tokens.CreateAccessToken(refreshToken.Id);
SetRefreshToken(refreshToken);
return LogicResult<AccessToken>.Ok(accessToken);
}
public ILogicResult<AccessToken> Register(UserEditor editor) {
var users = _users.GetUsers();
if (users.Any(user => user.Email == editor.Email || user.Username == editor.Username))
return LogicResult<AccessToken>.Conflict(_messages.UsernameOrEmailExist);
if (!ValidateUserdata(editor))
return LogicResult<AccessToken>.BadRequest(_messages.InvalidRegisterData);
var user = _users.CreateUser(editor);
var refreshToken = _tokens.CreateRefreshToken(user.Id);
var accessToken = _tokens.CreateAccessToken(refreshToken.Id);
SetRefreshToken(refreshToken);
return LogicResult<AccessToken>.Ok(accessToken);
}
public ILogicResult Logout(Guid userId) {
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
_tokens.DeleteUserTokens(userId);
if (_context.UserId == userId) DeleteRefreshToken();
return LogicResult.Ok();
}
public ILogicResult<AccessToken> GenerateToken(Guid refreshTokenId) {
if (!_tokens.ValidateRefreshToken(refreshTokenId)) return LogicResult<AccessToken>.Conflict(_messages.InvalidRefreshToken);
var token = _tokens.GetAccessTokens(refreshTokenId).ToArray().FirstOrDefault(token => _tokens.ValidateAccessToken(token.Id));
if (token is not null) return LogicResult<AccessToken>.Ok(token);
return LogicResult<AccessToken>.Ok(_tokens.CreateAccessToken(refreshTokenId));
}
public Guid GetCurrentUserRefreshToken() {
var token = _contextAccessor.HttpContext?.Request.Cookies["refresh_token"];
if (token == null) return Guid.Empty;
return Guid.Parse(token);
}
private bool ValidateUserdata(UserEditor editor) {
if (string.IsNullOrEmpty(editor.FirstName)) return false;
if (string.IsNullOrEmpty(editor.LastName)) return false;
if (string.IsNullOrEmpty(editor.Email)) return false;
if (string.IsNullOrEmpty(editor.Username)) return false;
if (string.IsNullOrEmpty(editor.Password)) return false;
if (editor.FirstName.Length > 255) return false;
if (editor.LastName.Length > 255) return false;
if (editor.Email.Length > 255) return false;
if (editor.Username.Length > 255) return false;
if (editor.Password.Length > 255) return false;
if (!editor.Email.Contains('@') || !editor.Email.Contains('.')) return false;
if (editor.Username.Contains('@')) return false;
if (editor.Password.Length < 8) return false;
return true;
}
private bool ValidateEdit(UserEditor editor) {
if (editor.FirstName?.Length > 255) return false;
if (editor.LastName?.Length > 255) return false;
if (editor.Email?.Length > 255) return false;
if (editor.Username?.Length > 255) return false;
if (editor.Password?.Length > 255) return false;
if (!string.IsNullOrEmpty(editor.Email)) {
if (!editor.Email.Contains('@') || !editor.Email.Contains('.')) return false;
}
if (!string.IsNullOrEmpty(editor.Username)) {
if (editor.Username.Contains('@')) return false;
}
if (!string.IsNullOrEmpty(editor.Password)) {
if (editor.Password.Length < 8) return false;
}
return true;
}
private void DeleteRefreshToken() {
_contextAccessor.HttpContext?.Response.Cookies.Delete("refresh_token");
}
private void SetRefreshToken(RefreshToken token) {
_contextAccessor.HttpContext?.Response.Cookies.Append("refresh_token", token.Id.ToString(), new CookieOptions {
MaxAge = token.ExpirationDate - DateTime.Now,
HttpOnly = true,
Secure = true
});
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Net;
using Microsoft.AspNetCore.Mvc;
namespace Backend.LogicResults {
public static class ControllerBaseExtention {
public static ActionResult FromLogicResult(this ControllerBase controller, ILogicResult result) {
switch (result.State) {
case LogicResultState.Ok:
return controller.Ok();
case LogicResultState.BadRequest:
return controller.StatusCode((int)HttpStatusCode.BadRequest, result.Message);
case LogicResultState.Forbidden:
return controller.StatusCode((int)HttpStatusCode.Forbidden, result.Message);
case LogicResultState.NotFound:
return controller.StatusCode((int)HttpStatusCode.NotFound, result.Message);
case LogicResultState.Conflict:
return controller.StatusCode((int)HttpStatusCode.Conflict, result.Message);
default:
throw new Exception("An unhandled result has occurred as a result of a service call.");
}
}
public static ActionResult FromLogicResult<T>(this ControllerBase controller, ILogicResult<T> result) {
switch (result.State) {
case LogicResultState.Ok:
return controller.Ok(result.Data);
case LogicResultState.BadRequest:
return controller.StatusCode((int)HttpStatusCode.BadRequest, result.Message);
case LogicResultState.Forbidden:
return controller.StatusCode((int)HttpStatusCode.Forbidden, result.Message);
case LogicResultState.NotFound:
return controller.StatusCode((int)HttpStatusCode.NotFound, result.Message);
case LogicResultState.Conflict:
return controller.StatusCode((int)HttpStatusCode.Conflict, result.Message);
default:
throw new Exception("An unhandled result has occurred as a result of a service call.");
}
}
}
}

View File

@@ -0,0 +1,19 @@
namespace Backend.LogicResults {
public interface ILogicResult {
LogicResultState State { get; set; }
string Message { get; set; }
bool IsSuccessful { get; }
}
public interface ILogicResult<T> {
LogicResultState State { get; set; }
T Data { get; set; }
string Message { get; set; }
bool IsSuccessful { get; }
}
}

View File

@@ -0,0 +1,170 @@
namespace Backend.LogicResults {
internal class LogicResult : ILogicResult {
public LogicResultState State { get; set; }
public string Message { get; set; }
public bool IsSuccessful => State == LogicResultState.Ok;
public static LogicResult Ok() {
return new LogicResult() {
State = LogicResultState.Ok
};
}
public static LogicResult BadRequest() {
return new LogicResult() {
State = LogicResultState.BadRequest
};
}
public static LogicResult BadRequest(string message) {
return new LogicResult() {
State = LogicResultState.BadRequest,
Message = message
};
}
public static LogicResult Forbidden() {
return new LogicResult() {
State = LogicResultState.Forbidden
};
}
public static LogicResult Forbidden(string message) {
return new LogicResult() {
State = LogicResultState.Forbidden,
Message = message
};
}
public static LogicResult NotFound() {
return new LogicResult() {
State = LogicResultState.NotFound
};
}
public static LogicResult NotFound(string message) {
return new LogicResult() {
State = LogicResultState.NotFound,
Message = message
};
}
public static LogicResult Conflict() {
return new LogicResult() {
State = LogicResultState.Conflict
};
}
public static LogicResult Conflict(string message) {
return new LogicResult() {
State = LogicResultState.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
};
}
}
internal class LogicResult<T> : ILogicResult<T> {
public LogicResultState State { get; set; }
public T Data { get; set; }
public string Message { get; set; }
public bool IsSuccessful => State == LogicResultState.Ok;
public static LogicResult<T> Ok() {
return new LogicResult<T>() {
State = LogicResultState.Ok
};
}
public static LogicResult<T> Ok(T result) {
return new LogicResult<T>() {
State = LogicResultState.Ok,
Data = result
};
}
public static LogicResult<T> BadRequest() {
return new LogicResult<T>() {
State = LogicResultState.BadRequest
};
}
public static LogicResult<T> BadRequest(string message) {
return new LogicResult<T>() {
State = LogicResultState.BadRequest,
Message = message
};
}
public static LogicResult<T> Forbidden() {
return new LogicResult<T>() {
State = LogicResultState.Forbidden
};
}
public static LogicResult<T> Forbidden(string message) {
return new LogicResult<T>() {
State = LogicResultState.Forbidden,
Message = message
};
}
public static LogicResult<T> NotFound() {
return new LogicResult<T>() {
State = LogicResultState.NotFound
};
}
public static LogicResult<T> NotFound(string message) {
return new LogicResult<T>() {
State = LogicResultState.NotFound,
Message = message
};
}
public static LogicResult<T> Conflict() {
return new LogicResult<T>() {
State = LogicResultState.Conflict
};
}
public static LogicResult<T> Conflict(string message) {
return new LogicResult<T>() {
State = LogicResultState.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
};
}
}
}

View File

@@ -0,0 +1,9 @@
namespace Backend.LogicResults {
public enum LogicResultState {
Ok,
BadRequest,
Forbidden,
NotFound,
Conflict
}
}

View File

@@ -0,0 +1,12 @@
namespace Backend.Options;
public class UserMessageOptions : OptionsFromConfiguration {
public override string Position => "Messages:Users";
public string NotFound { get; set; }
public string InvalidEditData { get; set; }
public string InvalidRegisterData { get; set; }
public string WrongPassword { get; set; }
public string UsernameOrEmailExist { get; set; }
public string InvalidRefreshToken { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace Backend.Options;
public class UserOptions : OptionsFromConfiguration {
public override string Position => "Users";
public string[] DefaultPermissions { get; set; }
}

79
Backend/Program.cs Normal file
View File

@@ -0,0 +1,79 @@
using Backend;
using Backend.Logic;
using Backend.Options;
using Backend.Repositorys;
using Backend.Security;
using Backend.Security.Authentication;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DatabaseContext>();
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddScoped<ITokenContext, TokenContext>();
builder.Services.AddScoped<TokenRepository>();
builder.Services.AddScoped<GroupRepository>();
builder.Services.AddScoped<UserRepository>();
builder.Services.AddScoped<UserLogic>();
builder.Services.AddOptionsFromConfiguration<JwtTokenAuthenticationOptions>(builder.Configuration);
builder.Services.AddOptionsFromConfiguration<UserOptions>(builder.Configuration);
builder.Services.AddOptionsFromConfiguration<UserMessageOptions>(builder.Configuration);
builder.Services.AddCors();
builder.Services.AddAuthentication(JwtTokenAuthentication.Scheme).AddJwtTokenAuthentication(builder.Configuration);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c => {
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n
Enter 'Bearer' [space] and then your token in the text input below.",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement {{
new OpenApiSecurityScheme {
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
ArraySegment<string>.Empty
}});
});
var app = builder.Build();
GroupRepository.CompileGroups(app.Configuration);
if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors(
options => options
.WithOrigins(app.Configuration.GetSection("Origins").Get<string[]>())
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
);
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseWebSockets();
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:42992",
"sslPort": 44301
}
},
"profiles": {
"Backend": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5142",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,94 @@
using Backend.Entitys;
namespace Backend.Repositorys;
public class GroupRepository {
public static PermissionGroup[] Groups;
public static void CompileGroups(IConfiguration configuration) {
var groupsSections = configuration.GetSection("Groups").GetChildren();
List<PermissionGroup> groups = new List<PermissionGroup>();
foreach (var section in groupsSections) {
PermissionGroup group = new PermissionGroup();
group.Name = section.GetValue<string>("Name");
group.Permission = section.GetValue<string>("Permission");
group.Permissions = section.GetSection("Permissions").Get<string[]>();
group.Inherits = section.GetSection("Inherits").Get<string[]>();
groups.Add(group);
}
Groups = groups.ToArray();
}
private readonly DatabaseContext _context;
private readonly PermissionGroup[] _groups;
public GroupRepository(DatabaseContext context) {
_context = context;
_groups = Groups;
}
public PermissionGroup GetPermissionGroup(string name) {
return _groups.SingleOrDefault(group => group.Permission.Equals(name));
}
public PermissionGroup[] GetGroupsFromUser(Guid userId) {
Permission[] permissions = GetUserPermissionsRaw(userId).ToArray();
return ExtractGroups(permissions);
}
public PermissionGroup[] ExtractGroups(Permission[] permissions) {
List<PermissionGroup> permissionGroups = new List<PermissionGroup>();
foreach (var permission in permissions) {
if (permission.PermissionKey.StartsWith("group.")) {
foreach (var permissionGroup in _groups) {
if (permission.PermissionKey.Equals(permissionGroup.Permission)) {
permissionGroups.Add(permissionGroup);
if (permissionGroup.Inherits is not null) {
foreach (var inherit in permissionGroup.Inherits) {
permissionGroups.Add(GetPermissionGroup(inherit));
}
}
}
}
}
}
return permissionGroups.ToArray();
}
public IEnumerable<Permission> GetUserPermissions(Guid userId) {
List<Permission> permissions = GetUserPermissionsRaw(userId).ToList();
PermissionGroup[] groups = ExtractGroups(permissions.ToArray());
foreach (var group in groups) {
if (group.Permissions is null) continue;
permissions.AddRange(group.Permissions
.Select(perm => new Permission { Id = -1, UserId = userId, PermissionKey = perm }));
}
return permissions;
}
public IEnumerable<Permission> GetUserPermissionsRaw(Guid userId) {
return _context.Permissions.Where(permission => permission.UserId == userId);
}
public void AddPermissions(Guid userId, params string[] permissions) {
foreach (var permission in permissions) {
_context.Permissions.Add(new Permission
{ PermissionKey = permission, UserId = userId });
}
_context.SaveChanges();
}
public void DeletePermissions(Guid userId, params string[] permissions) {
foreach (var permission in permissions) {
_context.Permissions.RemoveRange(_context.Permissions.Where(perm =>
perm.UserId == userId && perm.PermissionKey == permission));
}
_context.SaveChanges();
}
}

View File

@@ -0,0 +1,92 @@
using System.Text;
using Backend.Entitys;
using Backend.Security.Authentication;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using Microsoft.Extensions.Options;
namespace Backend.Repositorys;
public class TokenRepository {
private readonly JwtTokenAuthenticationOptions _options;
private readonly DatabaseContext _context;
public TokenRepository(IOptions<JwtTokenAuthenticationOptions> options, DatabaseContext context) {
_options = options.Value;
_context = context;
}
public RefreshToken GetRefreshToken(Guid refreshTokenId) {
if (string.IsNullOrEmpty(refreshTokenId.ToString())) return null;
return _context.RefreshTokens.SingleOrDefault(token => token.Id == refreshTokenId);
}
public AccessToken GetAccessToken(Guid accessTokenId) {
if (string.IsNullOrEmpty(accessTokenId.ToString())) return null;
return _context.AccessTokens.SingleOrDefault(token => token.Id == accessTokenId);
}
public IEnumerable<AccessToken> GetAccessTokens(Guid refreshTokenId) {
if (string.IsNullOrEmpty(refreshTokenId.ToString())) return ArraySegment<AccessToken>.Empty;
return _context.AccessTokens.Where(token => token.RefreshTokenId == refreshTokenId);
}
public bool ValidateAccessToken(Guid accessTokenId) {
AccessToken token = GetAccessToken(accessTokenId);
if (token == null) return false;
TimeSpan span = token.ExpirationDate - DateTime.Now;
return span.TotalMilliseconds > 0;
}
public bool ValidateRefreshToken(Guid refreshTokenId) {
RefreshToken token = GetRefreshToken(refreshTokenId);
if (token == null) return false;
TimeSpan span = token.ExpirationDate - DateTime.Now;
return span.TotalMilliseconds > 0;
}
public RefreshToken CreateRefreshToken(Guid userId) {
RefreshToken token = new RefreshToken {
UserId = userId, Id = Guid.NewGuid(),
ExpirationDate = DateTime.Now.Add(new TimeSpan(int.Parse(_options.RefreshTokenExpirationTimeInHours), 0, 0))
};
_context.RefreshTokens.Add(token);
_context.SaveChanges();
return token;
}
public AccessToken CreateAccessToken(Guid refreshTokenId) {
AccessToken token = new AccessToken {
RefreshTokenId = refreshTokenId, Id = Guid.NewGuid(),
ExpirationDate = DateTime.Now
.Add(new TimeSpan(0, int.Parse(_options.AccessTokenExpirationTimeInMinutes), 0))
};
_context.AccessTokens.Add(token);
_context.SaveChanges();
return token;
}
public void DeleteUserTokens(Guid userId) {
List<RefreshToken> refreshTokens = _context.RefreshTokens.Where(token => token.UserId == userId).ToList();
refreshTokens.ForEach(token => DeleteRefreshToken(token.Id));
_context.SaveChanges();
}
public void DeleteRefreshToken(Guid refreshTokenId) {
_context.RefreshTokens.RemoveRange(_context.RefreshTokens.Where(token => token.Id == refreshTokenId));
_context.AccessTokens.RemoveRange(_context.AccessTokens.Where(token => token.RefreshTokenId == refreshTokenId));
}
public static string Hash128(string plainText, string salt) {
try {
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: plainText,
salt: Encoding.Default.GetBytes(salt),
prf: KeyDerivationPrf.HMACSHA256,
iterationCount: 100000,
numBytesRequested: 256 / 8
));
return hashed;
} catch (Exception) { return ""; }
}
}

View File

@@ -0,0 +1,68 @@
using Backend.Entitys;
using Backend.Options;
using Microsoft.Extensions.Options;
namespace Backend.Repositorys;
public class UserRepository {
private DatabaseContext _context;
private TokenRepository _tokens;
private GroupRepository _groups;
private UserOptions _options;
public UserRepository(DatabaseContext context, TokenRepository tokens, GroupRepository groups, IOptions<UserOptions> options) {
_context = context;
_tokens = tokens;
_groups = groups;
_options = options.Value;
}
public IEnumerable<User> GetUsers() => _context.Users.OrderBy(user => user.Created);
public User GetUser(Guid userId) => _context.Users.SingleOrDefault(user => user.Id == userId);
public User CreateUser(UserEditor editor) {
var user = new User {
Id = Guid.NewGuid(),
Created = DateTime.Now,
Email = editor.Email,
FirstName = editor.FirstName,
LastName = editor.LastName,
Password = TokenRepository.Hash128(editor.Password, editor.Email),
Username = editor.Username
};
_context.Users.Add(user);
_context.SaveChanges();
_groups.AddPermissions(user.Id, _options.DefaultPermissions);
return user;
}
public void EditUser(Guid userId, UserEditor editor) {
var user = GetUser(userId);
string SetValue(string orig, string input, string hashed = null) {
if (!string.IsNullOrEmpty(input))
return !string.IsNullOrEmpty(hashed) ? hashed : input;
return orig;
}
user.Email = SetValue(user.Email, editor.Email);
user.FirstName = SetValue(user.FirstName, editor.FirstName);
user.LastName = SetValue(user.LastName, editor.LastName);
user.Username = SetValue(user.Username, editor.Username);
user.Password = SetValue(user.Password, editor.Password, TokenRepository.Hash128(editor.Password, editor.Email));
_context.SaveChanges();
}
public void DeleteUser(Guid userId) {
_context.Users.Remove(_context.Users.Single(user => user.Id == userId));
_context.Permissions.RemoveRange(_context.Permissions.Where(perm => perm.UserId == userId));
_tokens.DeleteUserTokens(userId);
_context.SaveChanges();
}
}

View File

@@ -0,0 +1,5 @@
namespace Backend.Security.Authentication {
public static class JwtTokenAuthentication {
public const string Scheme = "JwtTokenAuthentication";
}
}

View File

@@ -0,0 +1,15 @@
using Backend.Options;
using Microsoft.AspNetCore.Authentication;
namespace Backend.Security.Authentication {
public static class JwtTokenAuthenticationExtensions {
public static AuthenticationBuilder AddJwtTokenAuthentication(this AuthenticationBuilder builder,
IConfiguration configuration) {
builder.Services.AddOptionsFromConfiguration<JwtTokenAuthenticationOptions>(configuration);
return builder.AddScheme<JwtTokenAuthenticationHandlerOptions, JwtTokenAuthenticationHandler>(
JwtTokenAuthentication.Scheme,
_ => { });
}
}
}

View File

@@ -0,0 +1,88 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Text.Encodings.Web;
using System.Security.Claims;
using Backend.Entitys;
using Backend.Repositorys;
using Backend.Security.Authorization;
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
namespace Backend.Security.Authentication {
public class JwtTokenAuthenticationHandler : AuthenticationHandler<JwtTokenAuthenticationHandlerOptions> {
private readonly TokenRepository _tokens;
private readonly GroupRepository _groups;
private readonly JwtTokenAuthenticationOptions _options;
public JwtTokenAuthenticationHandler(
IOptionsMonitor<JwtTokenAuthenticationHandlerOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IOptions<JwtTokenAuthenticationOptions> tokenOptions,
TokenRepository tokens,
GroupRepository groups)
: base(options, logger, encoder, clock) {
_options = tokenOptions.Value;
_tokens = tokens;
_groups = groups;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (Request.Headers["Authorization"].Equals(_options.DebugAccessToken))
return AuthenticateResult.Success(GetAuthenticationTicket(null, null, "*"));
var accessToken = GetAccessToken();
if (accessToken == null) return AuthenticateResult.Fail("Access Token invalid");
var refreshToken = _tokens.GetRefreshToken(accessToken.RefreshTokenId);
if (refreshToken == null) return AuthenticateResult.Fail("Refresh Token invalid");
if (!_tokens.ValidateRefreshToken(refreshToken.Id)) return AuthenticateResult.Fail("Refresh Token invalid");
bool valid = _tokens.ValidateAccessToken(accessToken.Id);
return valid
? AuthenticateResult.Success(GetAuthenticationTicket(accessToken, refreshToken))
: AuthenticateResult.Fail("Access Token invalid");
}
private AuthenticationTicket GetAuthenticationTicket(AccessToken accessToken, RefreshToken refreshToken, params string[] customPerms) {
List<Claim> claims = GenerateClaims(accessToken, refreshToken, customPerms);
ClaimsPrincipal principal = new ClaimsPrincipal();
principal.AddIdentity(new ClaimsIdentity(claims, JwtTokenAuthentication.Scheme));
AuthenticationTicket ticket = new AuthenticationTicket(principal, Scheme.Name);
return ticket;
}
private List<Claim> GenerateClaims(AccessToken accessToken, RefreshToken refreshToken, params string[] customPerms) {
List<Claim> claims = new List<Claim>();
if (accessToken is not null && refreshToken is not null) {
claims.AddRange(new List<Claim> {
new(CustomClaimTypes.AccessTokenId, accessToken.Id.ToString()),
new(CustomClaimTypes.RefreshTokenId, refreshToken.Id.ToString()),
new(CustomClaimTypes.UserId, refreshToken.UserId.ToString()),
});
string[] permissions = _groups.GetUserPermissions(refreshToken.UserId).Select(perm => perm.PermissionKey).ToArray();
claims.AddRange(permissions
.Select(permission => new Claim(CustomClaimTypes.Permission, permission)));
}
claims.AddRange(customPerms.Select(perm => new Claim(CustomClaimTypes.Permission, perm)));
return claims;
}
private AccessToken GetAccessToken() {
string key = Request.Headers["Authorization"];
if (string.IsNullOrEmpty(key)) {
key = Request.Query["token"];
}
if (string.IsNullOrEmpty(key))
return null;
AccessToken token = _tokens.GetAccessToken(Guid.Parse(key));
return token;
}
}
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authentication;
namespace Backend.Security.Authentication {
public class JwtTokenAuthenticationHandlerOptions : AuthenticationSchemeOptions {
// Options for the authentication handler.
// Currently: None
}
}

View File

@@ -0,0 +1,11 @@
using Backend.Options;
namespace Backend.Security.Authentication {
public class JwtTokenAuthenticationOptions : OptionsFromConfiguration {
public override string Position => "Authentication";
public string RefreshTokenExpirationTimeInHours { get; set; }
public string AccessTokenExpirationTimeInMinutes { get; set; }
public string DebugAccessToken { get; set; }
}
}

View File

@@ -0,0 +1,5 @@
namespace Backend.Options {
public abstract class OptionsFromConfiguration {
public abstract string Position { get; }
}
}

View File

@@ -0,0 +1,17 @@
namespace Backend.Options {
public static class OptionsFromConfigurationExtensions {
public static T AddOptionsFromConfiguration<T>(this IServiceCollection services, IConfiguration configuration)
where T : OptionsFromConfiguration {
T optionsInstance = (T)Activator.CreateInstance(typeof(T));
if (optionsInstance == null) return null;
string position = optionsInstance.Position;
services.Configure((Action<T>)(options => {
IConfigurationSection section = configuration.GetSection(position);
if (section != null) {
section.Bind(options);
}
}));
return optionsInstance;
}
}
}

View File

@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Mvc;
namespace Backend.Security.Authorization {
public sealed class AuthorizedAttribute : TypeFilterAttribute {
public AuthorizedAttribute(params string[] permission) : base(typeof(AuthorizedFilter)) {
Arguments = new object[] { permission };
}
}
}

View File

@@ -0,0 +1,88 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Backend.Security.Authorization {
public class AuthorizedFilter : IAuthorizationFilter {
private readonly string[] _permissions;
public AuthorizedFilter(params string[] permissions) {
_permissions = permissions;
}
public void OnAuthorization(AuthorizationFilterContext context) {
if (EndpointHasAllowAnonymousFilter(context)) {
return;
}
if (!IsAuthenticated(context)) {
context.Result = new UnauthorizedResult();
return;
}
if (!ContainsRequiredRole(context)) {
context.Result = new ForbidResult();
return;
}
}
private static bool EndpointHasAllowAnonymousFilter(AuthorizationFilterContext context) {
return context.Filters.Any(item => item is IAllowAnonymousFilter);
}
private bool IsAuthenticated(AuthorizationFilterContext context) {
return context.HttpContext.User.Identity.IsAuthenticated;
}
private bool ContainsRequiredRole(AuthorizationFilterContext context) {
if (context.HttpContext.User.HasClaim(CustomClaimTypes.Permission, "*"))
return true;
var perms = context.HttpContext.User.Claims
.Where(c => c.Type == CustomClaimTypes.Permission)
.Select(c => c.Value).ToArray();
if (context.RouteData.Values.ContainsKey("userId")) {
var accessedUser = context.RouteData.Values["userId"] as string;
if (accessedUser == context.HttpContext.User.GetUserId()) {
var selfPerms = _permissions.Where(p => p.StartsWith("self.")).ToArray();
if (!selfPerms.Any())
return true;
if (CheckPermission(selfPerms, perms))
return true;
}
}
if (CheckPermission(_permissions, perms.Where(p => !p.StartsWith("self.")).ToArray()))
return true;
return false;
bool CheckPermission(string[] permissions, string[] permission) {
if (permissions.Length == 0)
return true;
if (permission.Contains("*"))
return true;
foreach (var perm in permissions) {
if (permission.Contains(perm))
return true;
string[] splice = perm.Split(".");
string cache = "";
foreach (var s in splice) {
cache += s + ".";
if (permission.Contains(cache + "*"))
return true;
}
}
return false;
}
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Linq;
using System.Security.Claims;
namespace Backend.Security.Authorization {
public static class ClaimsPrincipalExtensions {
public static string GetAccessTokenId(this ClaimsPrincipal principal) =>
principal.FindFirstValue(CustomClaimTypes.AccessTokenId);
public static string GetRefreshTokenId(this ClaimsPrincipal principal) =>
principal.FindFirstValue(CustomClaimTypes.RefreshTokenId);
public static string GetUserId(this ClaimsPrincipal principal) =>
principal.FindFirstValue(CustomClaimTypes.UserId);
public static string[] GetPermissions(this ClaimsPrincipal principal) => principal.Claims
.Where(claim => claim.Type.Equals(CustomClaimTypes.Permission))
.Select(claim => claim.Value)
.ToArray();
}
}

View File

@@ -0,0 +1,8 @@
namespace Backend.Security.Authorization {
public static class CustomClaimTypes {
public const string AccessTokenId = "WebDesktop.AccessTokenId";
public const string RefreshTokenId = "WebDesktop.RefreshTokenId";
public const string UserId = "WebDesktop.UserId";
public const string Permission = "WebDesktop.Permission";
}
}

View File

@@ -0,0 +1,9 @@
namespace Backend.Security {
public interface ITokenContext {
bool IsAuthenticated { get; }
Guid UserId { get; }
Guid AccessTokenId { get; }
Guid RefreshTokenId { get; }
string[] Permissions { get; }
}
}

View File

@@ -0,0 +1,14 @@
namespace Backend.Security;
public static class Permissions {
public const string ShowUsers = "users.see";
public const string EditUsers = "users.edit";
public const string DeleteUsers = "users.delete";
public const string LogoutUsers = "users.logout";
public const string EditUserPermissions = "users.permissions.edit";
public const string ShowUserPermissions = "users.permissions.show";
public const string EditOwnPermissions = "self.permissions.edit";
}

View File

@@ -0,0 +1,26 @@
using Backend.Security.Authorization;
namespace Backend.Security {
internal class TokenContext : ITokenContext {
private readonly IHttpContextAccessor _accessor;
public TokenContext(IHttpContextAccessor accessor) {
_accessor = accessor;
}
public bool IsAuthenticated => _accessor.HttpContext?.User.Identity?.IsAuthenticated == true;
public Guid UserId => CreateGuild(_accessor.HttpContext?.User.GetUserId());
public Guid AccessTokenId => CreateGuild(_accessor.HttpContext?.User.GetAccessTokenId());
public Guid RefreshTokenId => CreateGuild(_accessor.HttpContext?.User.GetRefreshTokenId());
public string[] Permissions => _accessor.HttpContext?.User.GetPermissions();
private static Guid CreateGuild(string id) {
if (string.IsNullOrEmpty(id)) return Guid.Empty;
return Guid.Parse(id);
}
}
}

View File

@@ -0,0 +1,6 @@
{
"Origins": ["http://localhost:4200", "http://localhost:9876"],
"Authentication": {
"DebugAccessToken": "474a0461-37ec-4b11-aefe-00c423d1511e"
}
}

44
Backend/appsettings.json Normal file
View File

@@ -0,0 +1,44 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Backend.Security.Authentication.JwtTokenAuthenticationHandler": "None"
}
},
"MySQL": "SERVER=213.136.89.237;DATABASE=WebDesktop;UID=WebDesktop;PASSWORD=Hft6bP@V3IkYvqS1",
"Origins": ["https://desktop.leon-hoppe.de"],
"AllowedHosts": "*",
"Authentication": {
"RefreshTokenExpirationTimeInHours": 12,
"AccessTokenExpirationTimeInMinutes": 5,
"DebugAccessToken": null
},
"Users": {
"DefaultPermissions": ["group.user"]
},
"Groups": [
{
"Permission": "group.admin",
"Name": "Admin",
"Inherits": [],
"Permissions": ["*"]
},
{
"Permission": "group.user",
"Name": "User",
"Inherits": [],
"Permissions": []
}
],
"Messages": {
"Users": {
"NotFound": "This user does not exist",
"InvalidEditData": "Userdata does not match security rules",
"InvalidRegisterData": "Userdata does not match security rules",
"WrongPassword": "Wrong password",
"UsernameOrEmailExist": "This username or email already exist",
"InvalidRefreshToken": "Invalid RefreshToken"
}
}
}