Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/Api/Controllers/AuthController.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,8 +115,7 @@ public async Task<IActionResult> Logout()

return Ok();
}

[HttpPost("reset-password")]

public async Task<IActionResult> ResetPassword([FromBody] ResetPasswordRequest request)
{
if (string.IsNullOrEmpty(request.NewPassword))
Expand Down
4 changes: 4 additions & 0 deletions src/Api/appsettings.Development.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,10 @@
"TokenLifetime": "00:20:00",
"RefreshTokenLifetimeInDays": 3
},
"SecuritySettings": {
"Pepper": "1f952d7238f35083abc3d6bf28410702c65f54afc0be29af7f1c89f5859d1d53"
},

"MailSettings": {
"ClientUrl": "https://send.api.mailtrap.io/api/send",
"Token": "745f040659edff0ce87b545567da72d2",
Expand Down
3 changes: 3 additions & 0 deletions src/Api/appsettings.Testing.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,9 @@
"TokenLifetime": "00:20:00",
"RefreshTokenLifetimeInDays": 3
},
"SecuritySettings": {
"Pepper": "1f952d7238f35083abc3d6bf28410702c65f54afc0be29af7f1c89f5859d1d53"
},
"Seed": true,
"Serilog" : {
"MinimumLevel" : {
Expand Down
6 changes: 6 additions & 0 deletions src/Application/Common/Interfaces/ISecurityService.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
namespace Application.Common.Interfaces;

public interface ISecurityService
{
string Hash(string input, string salt);
}
7 changes: 7 additions & 0 deletions src/Application/Helpers/SecurityUtil.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,4 +19,11 @@ public static string Hash(string input)

return stringBuilder.ToString();
}

public static string HashPasswordWith(this string input, string salt, string pepper)
{
pepper = Convert.ToBase64String(Encoding.UTF8.GetBytes(pepper));
salt = Convert.ToBase64String(Encoding.UTF8.GetBytes(salt));
return Hash(salt + input + pepper);
}
}
4 changes: 4 additions & 0 deletions src/Application/Helpers/StringUtil.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,4 +16,8 @@ public static string RandomString(int n)

return stringBuilder.ToString();
}

public static string RandomPassword() => RandomString(8);

public static string RandomSalt() => RandomString(24);
}
11 changes: 8 additions & 3 deletions src/Application/Users/Commands/AddUser.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,11 +66,13 @@ public class AddUserCommandHandler : IRequestHandler<Command, UserDto>
{
private readonly IApplicationDbContext _context;
private readonly IMapper _mapper;
private readonly ISecurityService _securityService;

public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper)
public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper, ISecurityService securityService)
{
_context = context;
_mapper = mapper;
_securityService = securityService;
}

public async Task<UserDto> Handle(Command request, CancellationToken cancellationToken)
Expand All@@ -91,11 +93,14 @@ public async Task<UserDto> Handle(Command request, CancellationToken cancellatio
throw new KeyNotFoundException("Department does not exist.");
}

var password = StringUtil.RandomString(8);
var password = StringUtil.RandomPassword();
var salt = StringUtil.RandomSalt();

var entity = new User
{
Username = request.Username,
PasswordHash = SecurityUtil.Hash(password),
PasswordHash = _securityService.Hash(password, salt),
PasswordSalt = salt,
Email = request.Email,
FirstName = request.FirstName?.Trim(),
LastName = request.LastName?.Trim(),
Expand Down
1 change: 1 addition & 0 deletions src/Domain/Entities/User.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ public class User : BaseAuditableEntity
public string Username { get; set; } = null!;
public string Email { get; set; } = null!;
public string PasswordHash { get; set; } = null!;
public string PasswordSalt { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
public Department? Department { get; set; }
Expand Down
15 changes: 15 additions & 0 deletions src/Infrastructure/ConfigureServices.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,7 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti
services.AddJweAuthentication(configuration);

services.AddAuthorization();
services.AddSecurityService(configuration);

return services;
}
Expand DownExpand Up@@ -109,4 +110,18 @@ private static IServiceCollection AddMailService(this IServiceCollection service

return services;
}

private static IServiceCollection AddSecurityService(this IServiceCollection services, IConfiguration configuration)
{
var securitySettings = configuration.GetSection(nameof(SecuritySettings)).Get<SecuritySettings>();

services.Configure<SecuritySettings>(option =>
{
option.Pepper = securitySettings!.Pepper;
});

services.AddTransient<ISecurityService, SecurityService>();

return services;
}
}
12 changes: 8 additions & 4 deletions src/Infrastructure/Identity/IdentityService.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@ public class IdentityService : IIdentityService
private readonly RSA _encryptionKey;
private readonly ECDsa _signingKey;
private readonly IMapper _mapper;
private readonly SecuritySettings _securitySettings;

public IdentityService(
TokenValidationParameters tokenValidationParameters,
Expand All@@ -37,7 +38,8 @@ public IdentityService(
IAuthDbContext authDbContext,
RSA encryptionKey,
ECDsa signingKey,
IMapper mapper)
IMapper mapper,
IOptions<SecuritySettings> securitySettingsOptions)
{
_tokenValidationParameters = tokenValidationParameters;
_jweSettings = jweSettingsOptions.Value;
Expand All@@ -46,6 +48,7 @@ public IdentityService(
_encryptionKey = encryptionKey;
_signingKey = signingKey;
_mapper = mapper;
_securitySettings = securitySettingsOptions.Value;
}

public async Task<bool> Validate(string token, string refreshToken)
Expand DownExpand Up@@ -196,7 +199,7 @@ public async Task<AuthenticationResult> RefreshTokenAsync(string token, string r
.Include(x => x.Department)
.FirstOrDefault(x => x.Email!.Equals(email));

if (user is null || !user.PasswordHash.Equals(SecurityUtil.Hash(password)))
if (user is null || !user.PasswordHash.Equals(password.HashPasswordWith(user.PasswordSalt, _securitySettings.Pepper)))
{
throw new AuthenticationException("Username or password is invalid.");
}
Expand DownExpand Up@@ -255,8 +258,9 @@ public async Task ResetPassword(string token, string newPassword)
{
user.IsActivated = true;
}

user.PasswordHash = SecurityUtil.Hash(newPassword);
var salt = StringUtil.RandomSalt();
user.PasswordSalt = salt;
user.PasswordHash = newPassword.HashPasswordWith(salt, newPassword);
resetPasswordToken.IsInvalidated = true;
await _applicationDbContext.SaveChangesAsync(CancellationToken.None);
await _authDbContext.SaveChangesAsync(CancellationToken.None);
Expand Down
12 changes: 8 additions & 4 deletions src/Infrastructure/Persistence/ApplicationDbContextSeed.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,10 +14,11 @@ public class ApplicationDbContextSeed
public static async Task Seed(ApplicationDbContext context, IConfiguration configuration, ILogger logger)
{
if (!configuration.GetValue<bool>("Seed")) return;


var securitySettings = configuration.GetSection(nameof(SecuritySettings)).Get<SecuritySettings>();
try
{
await TrySeedAsync(context);
await TrySeedAsync(context, securitySettings!.Pepper);
}
catch (Exception ex)
{
Expand All@@ -26,19 +27,22 @@ public static async Task Seed(ApplicationDbContext context, IConfiguration confi
}
}

private static async Task TrySeedAsync(ApplicationDbContext context)
private static async Task TrySeedAsync(ApplicationDbContext context, string pepper)
{
var department = new Department()
{
Name = "Admin"
};

var salt = StringUtil.RandomSalt();

// Default users
var admin = new User
{
Username = "admin",
Email = "admin@profile.dev",
PasswordHash = SecurityUtil.Hash("admin"),
PasswordHash = "admin".HashPasswordWith(salt, pepper),
PasswordSalt = salt,
IsActive = true,
IsActivated = true,
Created = LocalDateTime.FromDateTime(DateTime.UtcNow),
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,10 @@ public void Configure(EntityTypeBuilder<User> builder)
builder.Property(x => x.PasswordHash)
.HasMaxLength(64)
.IsRequired();

builder.Property(x => x.PasswordSalt)
.HasMaxLength(32)
.IsRequired();

builder.Property(x => x.FirstName)
.HasMaxLength(50)
Expand Down
Loading