Release v1.0.0 #16
.envDbInitializer.csDependencyInjection.csnuget.config
.gitea/workflows
.gitignoreApiDto
ApiDto.csproj
Backend.slnDockerfileCommon
Requests
Responses
Endpoint
Backend.httpISaveSettings.cs
README.mdCommon
Attributes
BadRequestResponseAttribute.csCacheMaxAgeAttribute.csLocalhostAttribute.csMaintenanceModeIgnoreAttribute.csNotFoundResponseAttribute.csSwaggerDefaultAttribute.csTokenAuthenticationAttribute.cs
Exceptions
Interfaces
MapperDto
AvailableProvidersConverter.csPairPeriodTimeConverter.csPasswordPolicyConverter.csTwoFactorAuthenticationConverter.cs
Services
Configuration
Core
BackgroundTasks
Middleware
CacheMaxAgeMiddleware.csCookieAuthorizationMiddleware.csCustomExceptionHandlerMiddleware.csJwtRevocationMiddleware.csMaintenanceModeMiddleware.cs
Startup
Model
SwaggerOptions
Validation
Controllers
BaseController.cs
Endpoint.csprojProgram.csConfiguration
V1
AuthController.csCampusController.csDisciplineController.csFacultyController.csGroupController.csImportController.csLectureHallController.csProfessorController.csScheduleController.csSecurityController.cs
WeatherForecastController.csSync
WeatherForecast.cswwwroot
css
swagger
Security
Common
CookieNames.cs
DependencyInjection.csDomain
Caching
CookieOptionsParameters.csOAuth2
OAuthProvider.csOAuthUser.csPasswordPolicy.csRequestContextInfo.csTwoFactorAuthenticator.csUser.csInterfaces
Properties
Security.csprojServices
SqlData
Application
Application.csprojDependencyInjection.cs
Common
Cqrs
Campus
Queries
Discipline
Queries
Faculty
Queries
Group
Queries
LectureHall
Queries
Professor
Queries
GetProfessorDetails
GetProfessorDetailsBySearch
GetProfessorList
Schedule
Interfaces
Domain
Domain.csproj
Schedule
Migrations
MysqlMigrations
Migrations
20240601023106_InitialMigration.Designer.cs20240601023106_InitialMigration.cs20241027034820_RemoveUnusedRef.Designer.cs20241027034820_RemoveUnusedRef.csUberDbContextModelSnapshot.cs
MysqlMigrations.csprojPsqlMigrations
Migrations
20240601021702_InitialMigration.Designer.cs20240601021702_InitialMigration.cs20241027032753_RemoveUnusedRef.Designer.cs20241027032753_RemoveUnusedRef.csUberDbContextModelSnapshot.cs
PsqlMigrations.csprojSqliteMigrations
Persistence
Common
BaseDbContext.csConfigurationResolver.csDatabaseProvider.csDbContextFactory.csModelBuilderExtensions.cs
Contexts
Schedule
EntityTypeConfigurations
Persistence.csprojUberDbContext.cs
32
ApiDto/Common/PasswordPolicy.cs
Normal file
32
ApiDto/Common/PasswordPolicy.cs
Normal file
@ -0,0 +1,32 @@
|
||||
namespace Mirea.Api.Dto.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the password policy settings for user authentication.
|
||||
/// </summary>
|
||||
public class PasswordPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum length required for a password.
|
||||
/// </summary>
|
||||
public int MinimumLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether at least one letter is required in the password.
|
||||
/// </summary>
|
||||
public bool RequireLetter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the password must contain both lowercase and uppercase letters.
|
||||
/// </summary>
|
||||
public bool RequireLettersDifferentCase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether at least one digit is required in the password.
|
||||
/// </summary>
|
||||
public bool RequireDigit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether at least one special character is required in the password.
|
||||
/// </summary>
|
||||
public bool RequireSpecialCharacter { get; set; }
|
||||
}
|
23
Endpoint/Common/MapperDto/PasswordPolicyConverter.cs
Normal file
23
Endpoint/Common/MapperDto/PasswordPolicyConverter.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Mirea.Api.Dto.Common;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Common.MapperDto;
|
||||
|
||||
public static class PasswordPolicyConverter
|
||||
{
|
||||
public static Security.Common.Domain.PasswordPolicy ConvertFromDto(this PasswordPolicy policy) =>
|
||||
new(policy.MinimumLength,
|
||||
policy.RequireLetter,
|
||||
policy.RequireLettersDifferentCase,
|
||||
policy.RequireDigit,
|
||||
policy.RequireSpecialCharacter);
|
||||
|
||||
public static PasswordPolicy ConvertToDto(this Security.Common.Domain.PasswordPolicy policy) =>
|
||||
new()
|
||||
{
|
||||
MinimumLength = policy.MinimumLength,
|
||||
RequireLetter = policy.RequireLetter,
|
||||
RequireDigit = policy.RequireDigit,
|
||||
RequireSpecialCharacter = policy.RequireSpecialCharacter,
|
||||
RequireLettersDifferentCase = policy.RequireLettersDifferentCase
|
||||
};
|
||||
}
|
@ -25,6 +25,7 @@ using System.Linq;
|
||||
using System.Net.Mail;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using PasswordPolicy = Mirea.Api.Dto.Common.PasswordPolicy;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Controllers.Configuration;
|
||||
|
||||
@ -340,6 +341,20 @@ public class SetupController(
|
||||
|
||||
return true;
|
||||
}
|
||||
[HttpPost("SetPasswordPolicy")]
|
||||
[TokenAuthentication]
|
||||
public ActionResult<bool> SetPasswordPolicy([FromBody] PasswordPolicy? policy = null)
|
||||
{
|
||||
GeneralConfig.PasswordPolicy = policy?.ConvertFromDto() ?? new Security.Common.Domain.PasswordPolicy();
|
||||
cache.Set("password", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
[HttpGet("PasswordPolicyConfiguration")]
|
||||
[TokenAuthentication]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult<PasswordPolicy> PasswordPolicyConfiguration() =>
|
||||
cache.TryGetValue("password", out _) ? Ok(GeneralConfig.PasswordPolicy) : NoContent();
|
||||
|
||||
[HttpPost("Submit")]
|
||||
[TokenAuthentication]
|
||||
|
@ -149,13 +149,16 @@ public class AuthController(IOptionsSnapshot<Admin> user, AuthService auth, Pass
|
||||
[BadRequestResponse]
|
||||
public ActionResult<string> RenewPassword([FromBody] string? password = null)
|
||||
{
|
||||
var passwordPolicy = generalConfig.Value.PasswordPolicy;
|
||||
var passwordPolicyService = new PasswordPolicyService(passwordPolicy);
|
||||
|
||||
if (string.IsNullOrEmpty(password))
|
||||
password = string.Empty;
|
||||
else if (!PasswordHashService.HasPasswordInPolicySecurity(password))
|
||||
throw new ControllerArgumentException("The password must be at least 8 characters long and contain at least one uppercase letter and one special character.");
|
||||
else
|
||||
passwordPolicyService.ValidatePasswordOrThrow(password);
|
||||
|
||||
while (!PasswordHashService.HasPasswordInPolicySecurity(password))
|
||||
password = GeneratorKey.GenerateAlphaNumeric(16, includes: "!@#%^");
|
||||
while (!passwordPolicyService.TryValidatePassword(password))
|
||||
password = GeneratorKey.GenerateAlphaNumeric(passwordPolicy.MinimumLength + 2, includes: "!@#%^");
|
||||
|
||||
var (salt, hash) = passwordService.HashPassword(password);
|
||||
|
||||
|
26
Endpoint/Controllers/V1/SecurityController.cs
Normal file
26
Endpoint/Controllers/V1/SecurityController.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Mirea.Api.Dto.Common;
|
||||
using Mirea.Api.Endpoint.Common.Attributes;
|
||||
using Mirea.Api.Endpoint.Common.MapperDto;
|
||||
using Mirea.Api.Endpoint.Configuration.Model;
|
||||
using QRCoder;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class SecurityController(IOptionsSnapshot<GeneralConfig> generalConfig) : BaseController
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the current password policy for user authentication.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The current password policy
|
||||
/// </returns>
|
||||
[HttpGet("PasswordPolicy")]
|
||||
public ActionResult<PasswordPolicy> PasswordPolicy() =>
|
||||
Ok(generalConfig.Value.PasswordPolicy.ConvertToDto());
|
||||
}
|
15
Security/Common/Domain/PasswordPolicy.cs
Normal file
15
Security/Common/Domain/PasswordPolicy.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace Mirea.Api.Security.Common.Domain;
|
||||
|
||||
public class PasswordPolicy(
|
||||
int minimumLength = 8,
|
||||
bool requireLetter = true,
|
||||
bool requireLettersDifferentCase = true,
|
||||
bool requireDigit = true,
|
||||
bool requireSpecialCharacter = true)
|
||||
{
|
||||
public int MinimumLength { get; set; } = minimumLength;
|
||||
public bool RequireLetter { get; set; } = requireLetter;
|
||||
public bool RequireLettersDifferentCase { get; set; } = requireLettersDifferentCase;
|
||||
public bool RequireDigit { get; set; } = requireDigit;
|
||||
public bool RequireSpecialCharacter { get; set; } = requireSpecialCharacter;
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
using Konscious.Security.Cryptography;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Mirea.Api.Security.Services;
|
||||
|
||||
public partial class PasswordHashService
|
||||
public class PasswordHashService
|
||||
{
|
||||
public int SaltSize { private get; init; }
|
||||
public int HashSize { private get; init; }
|
||||
@ -54,15 +53,4 @@ public partial class PasswordHashService
|
||||
|
||||
public bool VerifyPassword(string password, string saltBase64, string hashBase64) =>
|
||||
VerifyPassword(password, Convert.FromBase64String(saltBase64), Convert.FromBase64String(hashBase64));
|
||||
|
||||
public static bool HasPasswordInPolicySecurity(string password) =>
|
||||
password.Length >= 8 &&
|
||||
PasswordExistSpecialSymbol().IsMatch(password) &&
|
||||
PasswordExistUpperLetter().IsMatch(password);
|
||||
|
||||
[GeneratedRegex("[A-Z]+")]
|
||||
private static partial Regex PasswordExistUpperLetter();
|
||||
|
||||
[GeneratedRegex("[!@#$%^&*]+")]
|
||||
private static partial Regex PasswordExistSpecialSymbol();
|
||||
}
|
40
Security/Services/PasswordPolicyService.cs
Normal file
40
Security/Services/PasswordPolicyService.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using Mirea.Api.Security.Common.Domain;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
|
||||
namespace Mirea.Api.Security.Services;
|
||||
|
||||
public class PasswordPolicyService(PasswordPolicy policy)
|
||||
{
|
||||
public void ValidatePasswordOrThrow(string password)
|
||||
{
|
||||
if (password.Length < policy.MinimumLength)
|
||||
throw new SecurityException($"Password must be at least {policy.MinimumLength} characters long.");
|
||||
|
||||
if (policy.RequireLetter && !password.Any(char.IsLetter))
|
||||
throw new SecurityException("Password must contain at least one letter.");
|
||||
|
||||
if (policy.RequireLettersDifferentCase && !password.Any(char.IsLower) && !password.Any(char.IsUpper))
|
||||
throw new SecurityException("Password must contain at least one lowercase and uppercase letter.");
|
||||
|
||||
if (policy.RequireDigit && !password.Any(char.IsDigit))
|
||||
throw new SecurityException("Password must contain at least one digit.");
|
||||
|
||||
if (policy.RequireSpecialCharacter && password.All(char.IsLetterOrDigit))
|
||||
throw new SecurityException("Password must contain at least one special character.");
|
||||
}
|
||||
|
||||
public bool TryValidatePassword(string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
ValidatePasswordOrThrow(password);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user