diff --git a/Azaion.Api/Azaion.Api.csproj b/Azaion.Api/Azaion.Api.csproj
index 963b42e..faeea4e 100644
--- a/Azaion.Api/Azaion.Api.csproj
+++ b/Azaion.Api/Azaion.Api.csproj
@@ -8,6 +8,7 @@
+
diff --git a/Azaion.Api/Program.cs b/Azaion.Api/Program.cs
index c9dd83f..01a91f3 100644
--- a/Azaion.Api/Program.cs
+++ b/Azaion.Api/Program.cs
@@ -1,11 +1,41 @@
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
+using Azaion.Common;
using Azaion.Common.Configs;
using Azaion.Common.Database;
+using Azaion.Common.Entities;
using Azaion.Common.Requests;
using Azaion.Services;
using FluentValidation;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
+builder.Configuration.AddEnvironmentVariables();
+
+var jwtConfig = builder.Configuration.GetSection(nameof(JwtConfig)).Get();
+if (jwtConfig == null)
+ throw new Exception("Missing configuration section: JwtConfig");
+var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.Secret));
+
+builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(o =>
+ {
+ o.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = jwtConfig.Issuer,
+ ValidAudience = jwtConfig.Audience,
+ IssuerSigningKey = signingKey
+ };
+ });
+builder.Services.AddAuthorization();
+builder.Services.AddHttpContextAccessor();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
@@ -27,22 +57,57 @@ if (app.Environment.IsDevelopment())
}
app.UseHttpsRedirection();
+app.UseAuthentication();
+app.UseAuthorization();
+
+app.MapPost("/login",
+ async (string username, string password, IUserService userService, CancellationToken cancellationToken) =>
+ {
+ var user = await userService.ValidateUser(username, password);
+
+ var tokenHandler = new JwtSecurityTokenHandler();
+ var tokenDescriptor = new SecurityTokenDescriptor
+ {
+ Subject = new ClaimsIdentity([
+ new Claim(ClaimTypes.NameIdentifier, user.Id),
+ new Claim(ClaimTypes.Name, user.Email),
+ new Claim(ClaimTypes.Role, user.Role.ToString()),
+ new Claim(Constants.HARDWARE_ID, user.HardwareId)
+ ]),
+ Expires = DateTime.UtcNow.AddHours(2),
+ Issuer = jwtConfig.Issuer,
+ Audience = jwtConfig.Audience,
+ SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature)
+ };
+
+ var token = tokenHandler.CreateToken(tokenDescriptor);
+ var tokenString = tokenHandler.WriteToken(token);
+
+ return Results.Ok(new { Token = tokenString });
+ });
app.MapPost("/register-user",
async (RegisterUserRequest registerUserRequest, IUserService userService, CancellationToken cancellationToken)
- => await userService.RegisterUser(registerUserRequest, cancellationToken));
-
-app.MapPost("/resources/get",
- async (GetResourceRequest getResourceRequest, IUserService userService, IResourcesService resourcesService, CancellationToken cancellationToken) =>
- {
- await userService.ValidateUser(getResourceRequest, cancellationToken);
- var ms = new MemoryStream();
- await resourcesService.GetEncryptedResource(getResourceRequest, ms, cancellationToken);
- return ms;
- });
+ => await userService.RegisterUser(registerUserRequest, cancellationToken))
+ .RequireAuthorization(p => p.RequireRole(RoleEnum.ApiAdmin.ToString()));
app.MapPost("/resources",
async (UploadResourceRequest uploadResourceRequest, IResourcesService resourceService, CancellationToken cancellationToken)
- => await resourceService.SaveResource(uploadResourceRequest, cancellationToken));
+ => await resourceService.SaveResource(uploadResourceRequest, cancellationToken))
+ .RequireAuthorization(p => p.RequireRole(RoleEnum.ApiAdmin.ToString()));
+
+app.MapPost("/resources/get",
+ async (GetResourceRequest request, IUserService userService, IResourcesService resourcesService, CancellationToken cancellationToken) =>
+ {
+ var user = userService.CurrentUser;
+ if (user == null)
+ throw new BusinessException(ExceptionEnum.NoUser, "No current user");
+ if (string.IsNullOrEmpty(user.HardwareId))
+ await userService.UpdateHardwareId(user.Email, request.HardwareId);
+ var ms = new MemoryStream();
+ var key = Security.MakeEncryptionKey(user.Email, request.Password);
+ await resourcesService.GetEncryptedResource(request.ResourceEnum, key, ms, cancellationToken);
+ return ms;
+ }).RequireAuthorization();
app.Run();
\ No newline at end of file
diff --git a/Azaion.Common/BusinessException.cs b/Azaion.Common/BusinessException.cs
index 9146cc5..3c32844 100644
--- a/Azaion.Common/BusinessException.cs
+++ b/Azaion.Common/BusinessException.cs
@@ -8,6 +8,7 @@ public class BusinessException(ExceptionEnum exEnum, string message) : Exception
public enum ExceptionEnum
{
NoUserFound = 10,
+ NoUser = 15,
UserExists = 20,
PasswordIncorrect = 30,
UserLengthIncorrect = 33,
diff --git a/Azaion.Common/Configs/Constants.cs b/Azaion.Common/Configs/Constants.cs
new file mode 100644
index 0000000..f050783
--- /dev/null
+++ b/Azaion.Common/Configs/Constants.cs
@@ -0,0 +1,6 @@
+namespace Azaion.Common.Configs;
+
+public class Constants
+{
+ public const string HARDWARE_ID = nameof(HARDWARE_ID);
+}
\ No newline at end of file
diff --git a/Azaion.Common/Configs/JwtConfig.cs b/Azaion.Common/Configs/JwtConfig.cs
new file mode 100644
index 0000000..635d70f
--- /dev/null
+++ b/Azaion.Common/Configs/JwtConfig.cs
@@ -0,0 +1,8 @@
+namespace Azaion.Common.Configs;
+
+public class JwtConfig
+{
+ public string Issuer { get; set; } = null!;
+ public string Audience { get; set; } = null!;
+ public string Secret { get; set; }
+}
\ No newline at end of file
diff --git a/Azaion.Common/Entities/RoleEnum.cs b/Azaion.Common/Entities/RoleEnum.cs
index 07bb839..7c0f4ab 100644
--- a/Azaion.Common/Entities/RoleEnum.cs
+++ b/Azaion.Common/Entities/RoleEnum.cs
@@ -1,9 +1,10 @@
-namespace Azaion.Common;
+namespace Azaion.Common.Entities;
public enum RoleEnum
{
Operator,
Validator,
CompanionPC,
- Admin
+ Admin,
+ ApiAdmin
}
diff --git a/Azaion.Common/Entities/User.cs b/Azaion.Common/Entities/User.cs
index 335e799..c8b7a96 100644
--- a/Azaion.Common/Entities/User.cs
+++ b/Azaion.Common/Entities/User.cs
@@ -3,10 +3,8 @@
public class User
{
public string Id { get; set; } = null!;
- public string Username { get; set; } = null!;
+ public string Email { get; set; } = null!;
public string PasswordHash { get; set; } = null!;
public string HardwareId { get; set; } = null!;
public RoleEnum Role { get; set; }
-
- public string UniqueKey => $"Azaion#{Username}#{PasswordHash}#{HardwareId}";
}
\ No newline at end of file
diff --git a/Azaion.Common/Requests/GetResourceRequest.cs b/Azaion.Common/Requests/GetResourceRequest.cs
index 06e3052..6591ad6 100644
--- a/Azaion.Common/Requests/GetResourceRequest.cs
+++ b/Azaion.Common/Requests/GetResourceRequest.cs
@@ -4,9 +4,8 @@ namespace Azaion.Common.Requests;
public class GetResourceRequest
{
- public string Username { get; set; } = null!;
- public string Password { get; set; } = null!;
- public string HardwareId { get; set; } = null!;
+ public string Password { get; set; } = null!;
+ public string HardwareId { get; set; } = null!;
public ResourceEnum ResourceEnum { get; set; }
}
diff --git a/Azaion.Common/Requests/RegisterUserRequest.cs b/Azaion.Common/Requests/RegisterUserRequest.cs
index ad0508c..060acf8 100644
--- a/Azaion.Common/Requests/RegisterUserRequest.cs
+++ b/Azaion.Common/Requests/RegisterUserRequest.cs
@@ -1,3 +1,4 @@
+using Azaion.Common.Entities;
using FluentValidation;
namespace Azaion.Common.Requests;
diff --git a/Azaion.Services/Azaion.Services.csproj b/Azaion.Services/Azaion.Services.csproj
index 7feaa28..6429d28 100644
--- a/Azaion.Services/Azaion.Services.csproj
+++ b/Azaion.Services/Azaion.Services.csproj
@@ -11,6 +11,9 @@
+
+ C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.8\Microsoft.AspNetCore.Http.Abstractions.dll
+
C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.8\Microsoft.Extensions.Options.dll
diff --git a/Azaion.Services/ResourcesService.cs b/Azaion.Services/ResourcesService.cs
index ea63ba4..2ecb6e6 100644
--- a/Azaion.Services/ResourcesService.cs
+++ b/Azaion.Services/ResourcesService.cs
@@ -8,16 +8,15 @@ namespace Azaion.Services;
public interface IResourcesService
{
- Task GetEncryptedResource(GetResourceRequest request, Stream outputStream, CancellationToken cancellationToken = default);
+ Task GetEncryptedResource(ResourceEnum resource, string key, Stream outputStream, CancellationToken cancellationToken = default);
Task SaveResource(UploadResourceRequest request, CancellationToken cancellationToken = default);
}
public class ResourcesService(IOptions resourcesConfig) : IResourcesService
{
- public async Task GetEncryptedResource(GetResourceRequest request, Stream outputStream, CancellationToken cancellationToken = default)
+ public async Task GetEncryptedResource(ResourceEnum resource, string key, Stream outputStream, CancellationToken cancellationToken = default)
{
- var fileStream = new FileStream(GetResourcePath(request.ResourceEnum), FileMode.Open, FileAccess.Read);
- var key = Security.MakeEncryptionKey(request.Username, request.Password);
+ var fileStream = new FileStream(GetResourcePath(resource), FileMode.Open, FileAccess.Read);
await fileStream.EncryptTo(outputStream, key, cancellationToken);
}
diff --git a/Azaion.Services/UserService.cs b/Azaion.Services/UserService.cs
index 3038eae..045e63c 100644
--- a/Azaion.Services/UserService.cs
+++ b/Azaion.Services/UserService.cs
@@ -1,56 +1,80 @@
-using Azaion.Common;
+using System.Security.Claims;
+using Azaion.Common;
+using Azaion.Common.Configs;
using Azaion.Common.Database;
using Azaion.Common.Entities;
using Azaion.Common.Requests;
using LinqToDB;
+using Microsoft.AspNetCore.Http;
namespace Azaion.Services;
public interface IUserService
{
+ User? CurrentUser { get; }
Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default);
- Task ValidateUser(GetResourceRequest request, CancellationToken cancellationToken = default);
+ Task ValidateUser(string username, string password, string? hardwareId = null, CancellationToken cancellationToken = default);
+ Task UpdateHardwareId(string username, string hardwareId, CancellationToken cancellationToken = default);
}
-public class UserService(IDbFactory dbFactory) : IUserService
+public class UserService(IDbFactory dbFactory, IHttpContextAccessor httpContextAccessor) : IUserService
{
+ public User? CurrentUser
+ {
+ get
+ {
+ var claims = httpContextAccessor.HttpContext?.User.Claims.ToDictionary(x => x.Type);
+ if (claims == null)
+ return null;
+
+ Enum.TryParse(claims[ClaimTypes.Role].Value, out RoleEnum role);
+ return new User
+ {
+ Id = claims[ClaimTypes.NameIdentifier].Value,
+ Email = claims[ClaimTypes.Name].Value,
+ Role = role,
+ HardwareId = claims[Constants.HARDWARE_ID].Value,
+ };
+ }
+ }
+
public async Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default)
{
await dbFactory.Run(async db =>
{
- var existingUser = await db.Users.FirstOrDefaultAsync(u => u.Username == request.Email, token: cancellationToken);
+ var existingUser = await db.Users.FirstOrDefaultAsync(u => u.Email == request.Email, token: cancellationToken);
if (existingUser != null)
throw new BusinessException(ExceptionEnum.UserExists, "User already exists");
await db.InsertAsync(new User
{
- Username = request.Email,
+ Email = request.Email,
PasswordHash = request.Password.ToHash(),
Role = request.Role
}, token: cancellationToken);
});
}
- public async Task ValidateUser(GetResourceRequest request, CancellationToken cancellationToken = default) =>
+ public async Task ValidateUser(string username, string password, string? hardwareId = null, CancellationToken cancellationToken = default) =>
await dbFactory.Run(async db =>
{
- var user = await db.Users.FirstOrDefaultAsync(x => x.Username == request.Username, token: cancellationToken);
+ var user = await db.Users.FirstOrDefaultAsync(x => x.Email == username, token: cancellationToken);
if (user == null)
throw new BusinessException(ExceptionEnum.NoUserFound, "No user found");
- if (request.Password.ToHash() != user.PasswordHash)
+ if (password.ToHash() != user.PasswordHash)
throw new BusinessException(ExceptionEnum.PasswordIncorrect, "Passwords do not match");
- //If user's hardware Id is empty (usually on the first time login), then write down user
- if (string.IsNullOrEmpty(user.HardwareId))
- await db.Users.UpdateAsync(x => x.Username == request.Username, u => new User{HardwareId = request.HardwareId}, token: cancellationToken);
- else
- {
- //But if hardware Id exists, it should match with request
- if (user.HardwareId != request.HardwareId)
- throw new BusinessException(ExceptionEnum.HardwareIdMismatch, "Hardware id mismatch");
- }
+ if (user.Role == RoleEnum.ApiAdmin)
+ return user;
+ // For Non-API admins hardwareId should match if it was already set
+ if (user.HardwareId != null && user.HardwareId != hardwareId)
+ throw new BusinessException(ExceptionEnum.HardwareIdMismatch, "Hardware id mismatch");
return user;
});
+
+ public async Task UpdateHardwareId(string username, string hardwareId, CancellationToken cancellationToken = default) =>
+ await dbFactory.Run(async db =>
+ await db.Users.UpdateAsync(x => x.Email == username, u => new User { HardwareId = hardwareId}, token: cancellationToken));
}