add authorization

This commit is contained in:
Alex Bezdieniezhnykh
2024-11-11 21:07:28 +02:00
parent ca6175da7f
commit 85139b4fd2
12 changed files with 146 additions and 40 deletions
+1
View File
@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" /> <PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8"/> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
</ItemGroup> </ItemGroup>
+76 -11
View File
@@ -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.Configs;
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.Common.Entities;
using Azaion.Common.Requests; using Azaion.Common.Requests;
using Azaion.Services; using Azaion.Services;
using FluentValidation; using FluentValidation;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables();
var jwtConfig = builder.Configuration.GetSection(nameof(JwtConfig)).Get<JwtConfig>();
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.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
@@ -27,22 +57,57 @@ if (app.Environment.IsDevelopment())
} }
app.UseHttpsRedirection(); 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", app.MapPost("/register-user",
async (RegisterUserRequest registerUserRequest, IUserService userService, CancellationToken cancellationToken) async (RegisterUserRequest registerUserRequest, IUserService userService, CancellationToken cancellationToken)
=> await userService.RegisterUser(registerUserRequest, cancellationToken)); => await userService.RegisterUser(registerUserRequest, cancellationToken))
.RequireAuthorization(p => p.RequireRole(RoleEnum.ApiAdmin.ToString()));
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;
});
app.MapPost("/resources", app.MapPost("/resources",
async (UploadResourceRequest uploadResourceRequest, IResourcesService resourceService, CancellationToken cancellationToken) 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(); app.Run();
+1
View File
@@ -8,6 +8,7 @@ public class BusinessException(ExceptionEnum exEnum, string message) : Exception
public enum ExceptionEnum public enum ExceptionEnum
{ {
NoUserFound = 10, NoUserFound = 10,
NoUser = 15,
UserExists = 20, UserExists = 20,
PasswordIncorrect = 30, PasswordIncorrect = 30,
UserLengthIncorrect = 33, UserLengthIncorrect = 33,
+6
View File
@@ -0,0 +1,6 @@
namespace Azaion.Common.Configs;
public class Constants
{
public const string HARDWARE_ID = nameof(HARDWARE_ID);
}
+8
View File
@@ -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; }
}
+3 -2
View File
@@ -1,9 +1,10 @@
namespace Azaion.Common; namespace Azaion.Common.Entities;
public enum RoleEnum public enum RoleEnum
{ {
Operator, Operator,
Validator, Validator,
CompanionPC, CompanionPC,
Admin Admin,
ApiAdmin
} }
+1 -3
View File
@@ -3,10 +3,8 @@
public class User public class User
{ {
public string Id { get; set; } = null!; 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 PasswordHash { get; set; } = null!;
public string HardwareId { get; set; } = null!; public string HardwareId { get; set; } = null!;
public RoleEnum Role { get; set; } public RoleEnum Role { get; set; }
public string UniqueKey => $"Azaion#{Username}#{PasswordHash}#{HardwareId}";
} }
@@ -4,7 +4,6 @@ namespace Azaion.Common.Requests;
public class GetResourceRequest public class GetResourceRequest
{ {
public string Username { get; set; } = null!;
public string Password { get; set; } = null!; public string Password { get; set; } = null!;
public string HardwareId { get; set; } = null!; public string HardwareId { get; set; } = null!;
public ResourceEnum ResourceEnum { get; set; } public ResourceEnum ResourceEnum { get; set; }
@@ -1,3 +1,4 @@
using Azaion.Common.Entities;
using FluentValidation; using FluentValidation;
namespace Azaion.Common.Requests; namespace Azaion.Common.Requests;
+3
View File
@@ -11,6 +11,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http.Abstractions">
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.8\Microsoft.AspNetCore.Http.Abstractions.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Options"> <Reference Include="Microsoft.Extensions.Options">
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.8\Microsoft.Extensions.Options.dll</HintPath> <HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.8\Microsoft.Extensions.Options.dll</HintPath>
</Reference> </Reference>
+3 -4
View File
@@ -8,16 +8,15 @@ namespace Azaion.Services;
public interface IResourcesService 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); Task SaveResource(UploadResourceRequest request, CancellationToken cancellationToken = default);
} }
public class ResourcesService(IOptions<ResourcesConfig> resourcesConfig) : IResourcesService public class ResourcesService(IOptions<ResourcesConfig> 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 fileStream = new FileStream(GetResourcePath(resource), FileMode.Open, FileAccess.Read);
var key = Security.MakeEncryptionKey(request.Username, request.Password);
await fileStream.EncryptTo(outputStream, key, cancellationToken); await fileStream.EncryptTo(outputStream, key, cancellationToken);
} }
+41 -17
View File
@@ -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.Database;
using Azaion.Common.Entities; using Azaion.Common.Entities;
using Azaion.Common.Requests; using Azaion.Common.Requests;
using LinqToDB; using LinqToDB;
using Microsoft.AspNetCore.Http;
namespace Azaion.Services; namespace Azaion.Services;
public interface IUserService public interface IUserService
{ {
User? CurrentUser { get; }
Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default); Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default);
Task<User?> ValidateUser(GetResourceRequest request, CancellationToken cancellationToken = default); Task<User> 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) public async Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default)
{ {
await dbFactory.Run(async db => 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) if (existingUser != null)
throw new BusinessException(ExceptionEnum.UserExists, "User already exists"); throw new BusinessException(ExceptionEnum.UserExists, "User already exists");
await db.InsertAsync(new User await db.InsertAsync(new User
{ {
Username = request.Email, Email = request.Email,
PasswordHash = request.Password.ToHash(), PasswordHash = request.Password.ToHash(),
Role = request.Role Role = request.Role
}, token: cancellationToken); }, token: cancellationToken);
}); });
} }
public async Task<User?> ValidateUser(GetResourceRequest request, CancellationToken cancellationToken = default) => public async Task<User> ValidateUser(string username, string password, string? hardwareId = null, CancellationToken cancellationToken = default) =>
await dbFactory.Run(async db => 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) if (user == null)
throw new BusinessException(ExceptionEnum.NoUserFound, "No user found"); 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"); 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 (user.Role == RoleEnum.ApiAdmin)
if (string.IsNullOrEmpty(user.HardwareId)) return user;
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");
}
// 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; 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));
} }