diff --git a/SurveyBackend/SurveyBackend.API/Controllers/AuthController.cs b/SurveyBackend/SurveyBackend.API/Controllers/AuthController.cs new file mode 100644 index 0000000..93fb89a --- /dev/null +++ b/SurveyBackend/SurveyBackend.API/Controllers/AuthController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; +using SurveyBackend.DTOs; + +namespace SurveyBackend.Controllers; + +[ApiController] +[Route("auth")] +public class AuthController : ControllerBase +{ + [HttpPost("login")] + public async Task GetToken([FromBody] UserLoginDto loginData) + { + return Ok(); + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.API/DTOs/UserLoginDto.cs b/SurveyBackend/SurveyBackend.API/DTOs/UserLoginDto.cs new file mode 100644 index 0000000..8e58ee2 --- /dev/null +++ b/SurveyBackend/SurveyBackend.API/DTOs/UserLoginDto.cs @@ -0,0 +1,7 @@ +namespace SurveyBackend.DTOs; + +public record UserLoginDto +{ + public required string Email { get; set; } + public required string Password { get; set; } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.API/DTOs/UserRegistrationDto.cs b/SurveyBackend/SurveyBackend.API/DTOs/UserRegistrationDto.cs new file mode 100644 index 0000000..3c0808d --- /dev/null +++ b/SurveyBackend/SurveyBackend.API/DTOs/UserRegistrationDto.cs @@ -0,0 +1,10 @@ +namespace SurveyBackend.DTOs; + +public record UserRegistrationDto +{ + public string Email { get; set; } + public string Username { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Password { get; set; } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.API/Program.cs b/SurveyBackend/SurveyBackend.API/Program.cs index 3a98e82..23fd2d7 100644 --- a/SurveyBackend/SurveyBackend.API/Program.cs +++ b/SurveyBackend/SurveyBackend.API/Program.cs @@ -1,3 +1,10 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using SurveyBackend.Infrastructure; +using SurveyBackend.Infrastructure.Data; + namespace SurveyBackend; public class Program @@ -6,26 +13,48 @@ public class Program { var builder = WebApplication.CreateBuilder(args); - // Add services to the container. + AuthOptions.MakeOptions(builder.Configuration, Environment.GetEnvironmentVariable("JWT_SECRET_KEY")); + builder.Services.AddAuthorization(); - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddDbContext(options => + { + options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")); + }); + + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = AuthOptions.Issuer, + ValidAudience = AuthOptions.Audience, + IssuerSigningKey = AuthOptions.SymmetricSecurityKey + }; + }); + + builder.Services.AddControllers(); + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); - // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } - app.UseHttpsRedirection(); - + app.UseAuthentication(); app.UseAuthorization(); - + + app.MapControllers(); + app.Run(); } } \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.API/SurveyBackend.API.csproj b/SurveyBackend/SurveyBackend.API/SurveyBackend.API.csproj index 41529c3..3764109 100644 --- a/SurveyBackend/SurveyBackend.API/SurveyBackend.API.csproj +++ b/SurveyBackend/SurveyBackend.API/SurveyBackend.API.csproj @@ -8,8 +8,15 @@ + + + + + + + diff --git a/SurveyBackend/SurveyBackend.API/appsettings.Development.json b/SurveyBackend/SurveyBackend.API/appsettings.Development.json index 0c208ae..bcc29ea 100644 --- a/SurveyBackend/SurveyBackend.API/appsettings.Development.json +++ b/SurveyBackend/SurveyBackend.API/appsettings.Development.json @@ -4,5 +4,14 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "ConnectionStrings": { + "DefaultConnection": "Data Source=Application.db" + }, + "JwtSettings": { + "SecretKey": "sigma_super_secret_key_for_jwt_tokens_yo", + "Issuer": "SurveyBackend", + "Audience": "SurveyClient", + "ExpiresInMinutes": 600 } } diff --git a/SurveyBackend/SurveyBackend.API/appsettings.json b/SurveyBackend/SurveyBackend.API/appsettings.json index 10f68b8..3b14cf8 100644 --- a/SurveyBackend/SurveyBackend.API/appsettings.json +++ b/SurveyBackend/SurveyBackend.API/appsettings.json @@ -5,5 +5,14 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Data Source=Application.db" + }, + "JwtSettings": { + "SecretKey": "sigma_super_secret_key_for_jwt_tokens_yo_that_should_be_stored_in_ENV", + "Issuer": "SurveyBackend", + "Audience": "SurveyClient", + "ExpiresInMinutes": 600 + } } diff --git a/SurveyBackend/SurveyBackend.Core/Models/Group.cs b/SurveyBackend/SurveyBackend.Core/Models/Group.cs new file mode 100644 index 0000000..e74cb3f --- /dev/null +++ b/SurveyBackend/SurveyBackend.Core/Models/Group.cs @@ -0,0 +1,9 @@ +namespace SurveyBackend.Core.Models; + +public class Group +{ + public int Id { get; set; } + public string Label { get; set; } + + public ICollection Users { get; set; } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Core/Models/User.cs b/SurveyBackend/SurveyBackend.Core/Models/User.cs index 509c503..78f2286 100644 --- a/SurveyBackend/SurveyBackend.Core/Models/User.cs +++ b/SurveyBackend/SurveyBackend.Core/Models/User.cs @@ -1,11 +1,15 @@ +using Microsoft.AspNetCore.Identity; + namespace SurveyBackend.Core.Models; public class User { - public int Id { get; set; } - public string Username { get; set; } + public string Id { get; set; } public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } - public byte[] PasswordHash { get; set; } - public byte[] PasswordSalt { get; set; } + public string Password { get; set; } + + public ICollection Groups { get; set; } } \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Core/Repositories/IGenericRepository.cs b/SurveyBackend/SurveyBackend.Core/Repositories/IGenericRepository.cs new file mode 100644 index 0000000..228d3b1 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Core/Repositories/IGenericRepository.cs @@ -0,0 +1,10 @@ +namespace SurveyBackend.Core.Repositories; + +public interface IGenericRepository where T : class +{ + Task GetByIdAsync(int id); + Task> GetAllAsync(); + Task AddAsync(T entity); + Task UpdateAsync(T entity); + Task DeleteAsync(T entity); +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Core/Repositories/IUserRepository.cs b/SurveyBackend/SurveyBackend.Core/Repositories/IUserRepository.cs new file mode 100644 index 0000000..791059a --- /dev/null +++ b/SurveyBackend/SurveyBackend.Core/Repositories/IUserRepository.cs @@ -0,0 +1,7 @@ +using SurveyBackend.Core.Models; + +namespace SurveyBackend.Core.Repositories; + +public interface IUserRepository : IGenericRepository +{ +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Core/Services/IUserService.cs b/SurveyBackend/SurveyBackend.Core/Services/IUserService.cs new file mode 100644 index 0000000..7d97a29 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Core/Services/IUserService.cs @@ -0,0 +1,6 @@ +namespace SurveyBackend.Core.Services; + +public interface IUserService +{ + +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Core/SurveyBackend.Core.csproj b/SurveyBackend/SurveyBackend.Core/SurveyBackend.Core.csproj index 3a63532..adbb79f 100644 --- a/SurveyBackend/SurveyBackend.Core/SurveyBackend.Core.csproj +++ b/SurveyBackend/SurveyBackend.Core/SurveyBackend.Core.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/SurveyBackend/SurveyBackend.Infrastructure/AuthOptions.cs b/SurveyBackend/SurveyBackend.Infrastructure/AuthOptions.cs new file mode 100644 index 0000000..cd379e8 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Infrastructure/AuthOptions.cs @@ -0,0 +1,32 @@ +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace SurveyBackend.Infrastructure; + +public static class AuthOptions +{ + public static string Issuer; + public static string Audience; + public static TimeSpan TokenLifetime; + private static string? SecurityKey { get; set; } + + public static SymmetricSecurityKey SymmetricSecurityKey + { + get + { + ArgumentNullException.ThrowIfNull(SecurityKey); + + return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityKey)); + } + } + + public static void MakeOptions(IConfigurationManager configurationManager, string? securityKey = null) + { + var jwtSettings = configurationManager.GetSection("JwtSettings"); + Issuer = jwtSettings["Issuer"] ?? "DefaultIssuer"; + Audience = jwtSettings["Audience"] ?? "DefaultAudience"; + TokenLifetime = TimeSpan.FromMinutes(int.Parse(jwtSettings["TokenLifetime"] ?? "60")); + SecurityKey = securityKey ?? jwtSettings["SecretKey"]; + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Data/ApplicationDbContext.cs b/SurveyBackend/SurveyBackend.Infrastructure/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..fb500db --- /dev/null +++ b/SurveyBackend/SurveyBackend.Infrastructure/Data/ApplicationDbContext.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using SurveyBackend.Core.Models; + +namespace SurveyBackend.Infrastructure.Data; + +public class ApplicationDbContext : DbContext +{ + public DbSet Users { get; set; } + public DbSet Groups { get; set; } + + public ApplicationDbContext(DbContextOptions options) : base(options) + { + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Helpers/TokenHelper.cs b/SurveyBackend/SurveyBackend.Infrastructure/Helpers/TokenHelper.cs new file mode 100644 index 0000000..a52ae64 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Infrastructure/Helpers/TokenHelper.cs @@ -0,0 +1,31 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; +using SurveyBackend.Core.Models; + +namespace SurveyBackend.Infrastructure.Helpers; + +public class TokenHelper +{ + public static string GetAuthToken(User user) + { + var userId = user.Id.ToString(); + + var claims = new List + { + new(ClaimTypes.NameIdentifier, userId) + }; + + var jwt = new JwtSecurityToken( + claims: claims, + issuer: AuthOptions.Issuer, + audience: AuthOptions.Audience, + expires: DateTime.UtcNow + AuthOptions.TokenLifetime, + signingCredentials: new SigningCredentials(AuthOptions.SymmetricSecurityKey, SecurityAlgorithms.HmacSha256) + ); + + var token = new JwtSecurityTokenHandler().WriteToken(jwt); + + return token; + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Repositories/UserRepository.cs b/SurveyBackend/SurveyBackend.Infrastructure/Repositories/UserRepository.cs new file mode 100644 index 0000000..bd1980d --- /dev/null +++ b/SurveyBackend/SurveyBackend.Infrastructure/Repositories/UserRepository.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; +using SurveyBackend.Core.Models; +using SurveyBackend.Core.Repositories; +using SurveyBackend.Infrastructure.Data; + +namespace SurveyBackend.Infrastructure.Repositories; + +public class UserRepository : IUserRepository +{ + private readonly ApplicationDbContext _context; + + public UserRepository(ApplicationDbContext context) + { + _context = context; + } + + public async Task GetByIdAsync(int id) + { + return await _context.Users.FindAsync(id); + } + + public async Task> GetAllAsync() + { + return await _context.Users.ToListAsync(); + } + + public async Task AddAsync(User entity) + { + await _context.Users.AddAsync(entity); + await _context.SaveChangesAsync(); + } + + public async Task UpdateAsync(User entity) + { + _context.Users.Update(entity); + await _context.SaveChangesAsync(); + } + + public async Task DeleteAsync(User entity) + { + _context.Users.Remove(entity); + await _context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Services/IPasswordHasher.cs b/SurveyBackend/SurveyBackend.Infrastructure/Services/IPasswordHasher.cs new file mode 100644 index 0000000..52d1427 --- /dev/null +++ b/SurveyBackend/SurveyBackend.Infrastructure/Services/IPasswordHasher.cs @@ -0,0 +1,7 @@ +namespace SurveyBackend.Infrastructure.Services; + +public interface IPasswordHasher +{ + public string HashPassword(string password); + public bool Verify(string password, string hashedPassword); +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Infrastructure/Services/Sha256PasswordHasher.cs b/SurveyBackend/SurveyBackend.Infrastructure/Services/Sha256PasswordHasher.cs new file mode 100644 index 0000000..f513cff --- /dev/null +++ b/SurveyBackend/SurveyBackend.Infrastructure/Services/Sha256PasswordHasher.cs @@ -0,0 +1,61 @@ +using System.Security.Cryptography; + +namespace SurveyBackend.Infrastructure.Services; + +public class Sha256PasswordHasher : IPasswordHasher +{ + private const int RehashCount = 10000; + + private const int HashSizeBytes = 16; + + private const int SaltSizeBytes = 16; + + public string HashPassword(string password) + { + var saltBytes = RandomNumberGenerator.GetBytes(SaltSizeBytes); + + using var deriveBytes = GetDeriveBytes(password, saltBytes); + + var hashBytes = deriveBytes.GetBytes(HashSizeBytes); + + var fullHashBytes = new byte[SaltSizeBytes + HashSizeBytes]; + + Array.Copy(saltBytes, 0, fullHashBytes, 0, SaltSizeBytes); + Array.Copy(hashBytes, 0, fullHashBytes, SaltSizeBytes, HashSizeBytes); + + var hashedPassword = Convert.ToBase64String(fullHashBytes); + + return hashedPassword; + } + + public bool Verify(string password, string hashedPassword) + { + var fullHashBytes = Convert.FromBase64String(hashedPassword); + + var saltBytes = new byte[SaltSizeBytes]; + + Array.Copy(fullHashBytes, 0, saltBytes, 0, SaltSizeBytes); + + using var deriveBytes = GetDeriveBytes(password, saltBytes); + + var hashBytes = deriveBytes.GetBytes(HashSizeBytes); + + var isMatch = CompareHashes(hashBytes, fullHashBytes); + + return isMatch; + } + + private static bool CompareHashes(byte[] hashBytes, byte[] fullHashBytes) + { + for (var i = 0; i < HashSizeBytes; i++) + if (hashBytes[i] != fullHashBytes[i + SaltSizeBytes]) + return false; + + return true; + } + + private static Rfc2898DeriveBytes GetDeriveBytes(string password, byte[] saltBytes) + { + return new Rfc2898DeriveBytes(password, saltBytes, RehashCount, HashAlgorithmName.SHA256); + } +} \ No newline at end of file diff --git a/SurveyBackend/SurveyBackend.Infrastructure/SurveyBackend.Infrastructure.csproj b/SurveyBackend/SurveyBackend.Infrastructure/SurveyBackend.Infrastructure.csproj new file mode 100644 index 0000000..ce7dbfc --- /dev/null +++ b/SurveyBackend/SurveyBackend.Infrastructure/SurveyBackend.Infrastructure.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/SurveyBackend/SurveyBackend.sln b/SurveyBackend/SurveyBackend.sln index a7701f0..c79d7d1 100644 --- a/SurveyBackend/SurveyBackend.sln +++ b/SurveyBackend/SurveyBackend.sln @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyBackend.API", "Survey EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyBackend.Core", "SurveyBackend.Core\SurveyBackend.Core.csproj", "{596B4603-4066-4FF2-9C96-5357193F7229}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurveyBackend.Infrastructure", "SurveyBackend.Infrastructure\SurveyBackend.Infrastructure.csproj", "{4006471D-9F65-4AD6-852B-88A1211B49F4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ Global {596B4603-4066-4FF2-9C96-5357193F7229}.Debug|Any CPU.Build.0 = Debug|Any CPU {596B4603-4066-4FF2-9C96-5357193F7229}.Release|Any CPU.ActiveCfg = Release|Any CPU {596B4603-4066-4FF2-9C96-5357193F7229}.Release|Any CPU.Build.0 = Release|Any CPU + {4006471D-9F65-4AD6-852B-88A1211B49F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4006471D-9F65-4AD6-852B-88A1211B49F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4006471D-9F65-4AD6-852B-88A1211B49F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4006471D-9F65-4AD6-852B-88A1211B49F4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal