This commit is contained in:
Max 2025-01-27 01:21:57 +01:00 committed by GitHub
commit 2b76f79f24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
119 changed files with 1247 additions and 27 deletions

0
Blazor.Wasm/App.razor Normal file → Executable file
View File

0
Blazor.Wasm/Blazor.Wasm.csproj Normal file → Executable file
View File

0
Blazor.Wasm/Layout/MainLayout.razor Normal file → Executable file
View File

0
Blazor.Wasm/Layout/MainLayout.razor.css Normal file → Executable file
View File

0
Blazor.Wasm/Layout/NavMenu.razor Normal file → Executable file
View File

0
Blazor.Wasm/Layout/NavMenu.razor.css Normal file → Executable file
View File

0
Blazor.Wasm/Pages/Counter.razor Normal file → Executable file
View File

0
Blazor.Wasm/Pages/Home.razor Normal file → Executable file
View File

0
Blazor.Wasm/Pages/Weather.razor Normal file → Executable file
View File

0
Blazor.Wasm/Program.cs Normal file → Executable file
View File

0
Blazor.Wasm/Properties/launchSettings.json Normal file → Executable file
View File

0
Blazor.Wasm/_Imports.razor Normal file → Executable file
View File

0
Blazor.Wasm/wwwroot/css/app.css Normal file → Executable file
View File

0
Blazor.Wasm/wwwroot/css/bootstrap/bootstrap.min.css vendored Normal file → Executable file
View File

View File

0
Blazor.Wasm/wwwroot/favicon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

0
Blazor.Wasm/wwwroot/icon-192.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

0
Blazor.Wasm/wwwroot/index.html Normal file → Executable file
View File

0
Blazor.Wasm/wwwroot/sample-data/weather.json Normal file → Executable file
View File

0
DotBased.ASP.Auth/AuthDataCache.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/AuthenticationService.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/BasedAuthConfiguration.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/BasedAuthDefaults.cs Normal file → Executable file
View File

View File

View File

0
DotBased.ASP.Auth/Domains/Auth/PermissionModel.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/Domains/Auth/RoleModel.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/Domains/Identity/GroupItemModel.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/Domains/Identity/GroupModel.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/Domains/Identity/UserItemModel.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/Domains/Identity/UserModel.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/Domains/LoginModel.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/Domains/RegisterModel.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/DotBased.ASP.Auth.csproj Normal file → Executable file
View File

2
DotBased.ASP.Auth/DotBasedAuthDependencyInjection.cs Normal file → Executable file
View File

@ -30,7 +30,7 @@ public static class DotBasedAuthDependencyInjection
services.AddAuthentication(options => services.AddAuthentication(options =>
{ {
options.DefaultScheme = BasedAuthDefaults.AuthenticationScheme; options.DefaultScheme = BasedAuthDefaults.AuthenticationScheme;
});/*.AddScheme<BasedAuthenticationHandlerOptions, BasedAuthenticationHandler>(BasedAuthDefaults.AuthenticationScheme, null);*/ });
services.AddAuthorization(); services.AddAuthorization();
services.AddCascadingAuthenticationState(); services.AddCascadingAuthenticationState();
return services; return services;

0
DotBased.ASP.Auth/IAuthDataRepository.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/ISessionStateProvider.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/MemoryAuthDataRepository.cs Normal file → Executable file
View File

View File

View File

View File

View File

View File

View File

View File

0
DotBased.ASP.Auth/SecurityManager.cs Normal file → Executable file
View File

0
DotBased.ASP.Auth/SecurityService.cs Normal file → Executable file
View File

View File

@ -1,11 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace DotBased.AspNet.Auth;
public static class BasedAuthExtensions
{
public static IServiceCollection AddBasedAuthentication(this IServiceCollection services)
{
return services;
}
}

View File

@ -0,0 +1,10 @@
namespace DotBased.AspNet.Authority.Attributes;
/// <summary>
/// Indicates to protect the property before saving/loading to the repository.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ProtectAttribute : Attribute
{
}

View File

@ -0,0 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
namespace DotBased.AspNet.Authority;
public class AuthorityBuilder
{
public AuthorityBuilder(IServiceCollection services)
{
Services = services;
}
public IServiceCollection Services { get; }
}

View File

@ -0,0 +1,11 @@
namespace DotBased.AspNet.Authority;
public static class AuthorityDefaults
{
public static class Scheme
{
public const string AuthenticationScheme = "Authority.Scheme.Authentication";
public const string ExternalScheme = "Authority.Scheme.External";
}
}

View File

@ -0,0 +1,57 @@
using DotBased.AspNet.Authority.Crypto;
using DotBased.AspNet.Authority.Managers;
using DotBased.AspNet.Authority.Models.Options;
using DotBased.AspNet.Authority.Validators;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace DotBased.AspNet.Authority;
public static class AuthorityProviderExtensions
{
public static AuthorityBuilder AddAuthority(this IServiceCollection services, Action<AuthorityOptions>? optionsAction = null)
{
if (optionsAction != null)
{
services.AddOptions();
services.Configure<AuthorityOptions>(optionsAction);
}
services.TryAddScoped<ICryptographer, Cryptographer>();
services.TryAddScoped<IPasswordHasher, PasswordHasher>();
services.TryAddScoped<IPasswordValidator, PasswordOptionsValidator>();
services.TryAddScoped<IPasswordValidator, PasswordEqualsValidator>();
services.TryAddScoped<IUserValidator, UserValidator>();
/*services.TryAddScoped<IEmailVerifier, EmailVerifier>();
services.TryAddScoped<IPhoneNumberVerifier, PhoneNumberVerifier>();
services.TryAddScoped<IUserVerifier, UserVerifier>();*/
services.TryAddScoped<AuthorityManager>();
return new AuthorityBuilder(services);
}
public static AuthorityBuilder AddAuthorityRepository<TRepository>(this AuthorityBuilder authorityBuilder) where TRepository : class
{
return authorityBuilder;
}
public static AuthorityBuilder MapAuthorityEndpoints(this AuthorityBuilder builder)
{
return builder;
}
private static Type GetBaseGenericArgumentType<TModel>(Type baseType)
{
var userGenericBaseTypeDefinition = typeof(TModel).BaseType?.GetGenericTypeDefinition();
if (userGenericBaseTypeDefinition != null && userGenericBaseTypeDefinition == baseType)
{
var userBaseGenericArguments = userGenericBaseTypeDefinition.GetGenericArguments();
if (userBaseGenericArguments.Length <= 0)
{
throw new ArgumentException("Base implementation does not have the required generic argument.", nameof(TModel));
}
return userBaseGenericArguments[0];
}
throw new ArgumentException($"Given object {typeof(TModel).Name} does not have the base implementation type of: {baseType.Name}", nameof(TModel));
}
}

View File

@ -0,0 +1,14 @@
namespace DotBased.AspNet.Authority.Crypto;
public class Cryptographer : ICryptographer
{
public Task<string?> EncryptAsync(string data)
{
throw new NotImplementedException();
}
public Task<string?> DecryptAsync(string data)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,7 @@
namespace DotBased.AspNet.Authority.Crypto;
public interface ICryptographer
{
public Task<string?> EncryptAsync(string data);
public Task<string?> DecryptAsync(string data);
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Crypto;
public interface IPasswordHasher
{
public Task<string> HashPasswordAsync(string password);
}

View File

@ -0,0 +1,9 @@
namespace DotBased.AspNet.Authority.Crypto;
public class PasswordHasher : IPasswordHasher
{
public async Task<string> HashPasswordAsync(string password)
{
throw new NotImplementedException();
}
}

View File

@ -7,8 +7,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Models\" /> <Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
<Folder Include="Repositories\" /> <HintPath>..\..\..\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions\8.0.2\lib\net8.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -16,16 +17,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Microsoft.AspNetCore.Authentication"> <Folder Include="Models\Security\" />
<HintPath>..\..\..\..\..\usr\lib64\dotnet\shared\Microsoft.AspNetCore.App\8.0.11\Microsoft.AspNetCore.Authentication.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
<HintPath>..\..\..\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions\8.0.2\lib\net8.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.11" /> <PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,10 @@
namespace DotBased.AspNet.Authority.Managers;
public partial class AuthorityManager
{
/*
* - Validate User & Group
* - Check if user is already in group (if already in group return)
* - Add to UsersGroups table
*/
}

View File

@ -0,0 +1,97 @@
using System.Reflection;
using DotBased.AspNet.Authority.Attributes;
using DotBased.AspNet.Authority.Crypto;
using DotBased.AspNet.Authority.Models.Options;
using DotBased.AspNet.Authority.Repositories;
using DotBased.AspNet.Authority.Validators;
using DotBased.Logging;
using Microsoft.Extensions.Options;
namespace DotBased.AspNet.Authority.Managers;
public partial class AuthorityManager(
IOptions<AuthorityOptions> options,
IServiceProvider services,
ICryptographer cryptographer,
IUserRepository userRepository,
IRoleRepository roleRepository,
IPasswordHasher passwordHasher)
{
private readonly ILogger _logger = LogService.RegisterLogger<AuthorityManager>();
public IServiceProvider Services { get; } = services;
public AuthorityOptions Options { get; } = options.Value;
public ICryptographer Cryptographer { get; } = cryptographer;
public IUserRepository UserRepository { get; } = userRepository;
public IRoleRepository RoleRepository { get; } = roleRepository;
public IPasswordHasher PasswordHasher { get; } = passwordHasher;
public IEnumerable<IPasswordValidator> PasswordValidators { get; } = [];
public IEnumerable<IUserValidator> UserValidators { get; } = [];
public long GenerateVersion() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
/// <summary>
/// Protect or unprotect the properties with the <see cref="ProtectAttribute"/>
/// </summary>
/// <param name="data">The data model</param>
/// <param name="protection">True for protect false for unprotect.</param>
/// <typeparam name="TModel">The class with the properties to protect.</typeparam>
public async Task HandlePropertyProtection<TModel>(TModel data, bool protection)
{
var props = GetProtectedPropertiesValues(data);
if (props.Count == 0)
{
return;
}
var handledProperties = 0;
foreach (var property in props)
{
if (property.PropertyType != typeof(string))
{
_logger.Warning("Property({PropName}) with type: {PropType} detected, encrypting only supports strings! Skipping property!", property.Name, property.PropertyType);
continue;
}
string? cryptString;
if (protection)
{
cryptString = await Cryptographer.EncryptAsync(property.GetValue(data)?.ToString() ?? string.Empty);
}
else
{
cryptString = await Cryptographer.DecryptAsync(property.GetValue(data)?.ToString() ?? string.Empty);
}
if (cryptString == null)
{
_logger.Warning("{Protection} failed for property {PropName}", protection ? "Encryption" : "Decryption", property.Name);
continue;
}
property.SetValue(data, cryptString);
handledProperties++;
}
_logger.Debug("{HandledPropCount}/{TotalPropCount} protection properties handled!", handledProperties, props.Count);
}
public bool IsPropertyProtected<TModel>(string propertyName)
{
var protectedProperties = GetProtectedProperties<TModel>();
var propertyFound = protectedProperties.Where(propInfo => propInfo.Name == propertyName);
return propertyFound.Any();
}
public List<PropertyInfo> GetProtectedPropertiesValues<TModel>(TModel model)
{
var protectedProperties = GetProtectedProperties<TModel>();
return protectedProperties.Count != 0 ? protectedProperties : [];
}
public List<PropertyInfo> GetProtectedProperties<TModel>()
=> typeof(TModel).GetProperties().Where(p => Attribute.IsDefined(p, typeof(ProtectAttribute))).ToList();
}

View File

@ -0,0 +1,65 @@
using DotBased.AspNet.Authority.Models.Authority;
namespace DotBased.AspNet.Authority.Managers;
public partial class AuthorityManager
{
public async Task<Result<AuthorityRole>> CreateRoleAsync(AuthorityRole role, CancellationToken? cancellationToken = null)
{
return Result<AuthorityRole>.Failed("Not implemented!");
}
public async Task<Result> DeleteRoleAsync(AuthorityRole role, CancellationToken? cancellationToken = null)
{
return Result.Failed("Not implemented!");
}
public async Task<Result<AuthorityRole>> UpdateRoleAsync(AuthorityRole role, CancellationToken? cancellationToken = null)
{
return Result<AuthorityRole>.Failed("Not implemented!");
}
public async Task<ListResult<AuthorityRole>> GetRolesAsync(int limit = 20, int offset = 0, string search = "", CancellationToken? cancellationToken = null)
{
/*
* Search by role name & id
* Order by name, created date, creator? (paging)
*/
return ListResult<AuthorityRole>.Failed("Not implemented!");
}
public async Task AddRoleToUserAsync(AuthorityUser user, AuthorityRole role, CancellationToken? cancellationToken = null)
{
/*
- Validate User & Role
- Check if role is already in linked to user (if user already has the role, return)
- Add to UsersRoles table
*/
}
public async Task RemoveRoleFromUserAsync(AuthorityRole role, AuthorityUser user, CancellationToken? cancellationToken = null)
{
}
public async Task AddRoleToGroupAsync(AuthorityRole role, AuthorityGroup group, CancellationToken? cancellationToken = null)
{
}
/// <summary>
/// Get all roles (including group roles) that the user has.
/// </summary>
/// <param name="user">The user to get the roles from</param>
/// <param name="cancellationToken"></param>
public async Task<ListResult<AuthorityRole>> GetUserRolesAsync(AuthorityUser user, CancellationToken? cancellationToken = null)
{
/*
* - Validate user
* - Get user groups (id)
* - Get roles contained from user
* - Get roles contained from groups (if any)
* - Order by (for paging)
*/
return ListResult<AuthorityRole>.Failed("Not implemented!");
}
}

View File

@ -0,0 +1,97 @@
using DotBased.AspNet.Authority.Models;
using DotBased.AspNet.Authority.Models.Authority;
using DotBased.AspNet.Authority.Models.Validation;
namespace DotBased.AspNet.Authority.Managers;
public partial class AuthorityManager
{
public async Task<ValidationResult> ValidatePasswordAsync(AuthorityUser user, string password)
{
List<ValidationError> errors = [];
foreach (var validator in PasswordValidators)
{
var validatorResult = await validator.ValidatePasswordAsync(this, user, password);
if (!validatorResult.Success)
{
errors.AddRange(validatorResult.Errors);
}
}
return errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok();
}
public async Task<ValidationResult> ValidateUserAsync(AuthorityUser user)
{
List<ValidationError> errors = [];
foreach (var userValidator in UserValidators)
{
var validationResult = await userValidator.ValidateUserAsync(this, user);
if (!validationResult.Success)
{
errors.AddRange(validationResult.Errors);
}
}
return errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok();
}
public async Task<ListResult<AuthorityUser>> SearchUsersAsync(string query, int maxResults = 20, int offset = 0, CancellationToken? cancellationToken = null)
{
var searchResult = await UserRepository.GetAuthorityUsersAsync(query, maxResults, offset, cancellationToken);
return searchResult.Item1 == null ? ListResult<AuthorityUser>.Failed("No results!") : ListResult<AuthorityUser>.Ok(searchResult.Item1, searchResult.Item2);
}
public async Task<AuthorityResult<AuthorityUser>> UpdatePasswordAsync(AuthorityUser user, string password, CancellationToken? cancellationToken = null)
{
var passwordValidation = await ValidatePasswordAsync(user, password);
if (!passwordValidation.Success)
{
List<ValidationError> errors = [];
errors.AddRange(passwordValidation.Errors);
return AuthorityResult<AuthorityUser>.Failed(errors, ResultFailReason.Validation);
}
user.PasswordHash = await PasswordHasher.HashPasswordAsync(password);
user.SecurityVersion = GenerateVersion();
var updateResult = await UserRepository.UpdateUserAsync(user, cancellationToken);
return updateResult == null ? AuthorityResult<AuthorityUser>.Error("Failed to save updates!") : AuthorityResult<AuthorityUser>.Ok(updateResult);
}
public async Task<AuthorityResult<AuthorityUser>> CreateUserAsync(AuthorityUser userModel, string password, CancellationToken? cancellationToken = null)
{
var userValidation = await ValidateUserAsync(userModel);
var passwordValidation = await ValidatePasswordAsync(userModel, password);
if (!userValidation.Success || !passwordValidation.Success)
{
List<ValidationError> errors = [];
errors.AddRange(userValidation.Errors);
errors.AddRange(passwordValidation.Errors);
return AuthorityResult<AuthorityUser>.Failed(errors, ResultFailReason.Validation);
}
userModel.Version = GenerateVersion();
userModel.SecurityVersion = GenerateVersion();
var hashedPassword = await PasswordHasher.HashPasswordAsync(password);
userModel.PasswordHash = hashedPassword;
var userCreationResult = await UserRepository.CreateUserAsync(userModel, cancellationToken);
return userCreationResult != null
? AuthorityResult<AuthorityUser>.Ok(userCreationResult)
: AuthorityResult<AuthorityUser>.Error("Failed to create user in repository!");
}
public async Task<Result<AuthorityUser>> UpdateUserAsync(AuthorityUser model, CancellationToken? cancellationToken = null)
{
var updateResult = await UserRepository.UpdateUserAsync(model, cancellationToken);
return updateResult != null ? Result<AuthorityUser>.Ok(updateResult) : Result<AuthorityUser>.Failed("Failed to update user in repository!");
}
public async Task<bool> DeleteUserAsync(AuthorityUser model, CancellationToken? cancellationToken = null)
{
var deleteResult = await UserRepository.DeleteUserAsync(model, cancellationToken);
return deleteResult;
}
}

View File

@ -0,0 +1,26 @@
namespace DotBased.AspNet.Authority.Models.Authority;
public class AuthorityAttribute
{
public AuthorityAttribute(string attributeKey, string bound)
{
AttributeKey = attributeKey;
BoundId = bound;
}
public AuthorityAttribute()
{
AttributeKey = string.Empty;
BoundId = string.Empty;
}
public string AttributeKey { get; set; } // ClaimType/Authority.attribute.enabled
public string BoundId { get; set; } // Bound to User, Group, Role id
public object? AttributeValue { get; set; }
public string? Type { get; set; } // AspNet.Claim.Role/Property/Data.JSON, Data.Raw, Data.Base64 etc.
public long Version { get; set; }
}

View File

@ -0,0 +1,23 @@
namespace DotBased.AspNet.Authority.Models.Authority;
public class AuthorityGroup
{
public AuthorityGroup(string name) : this()
{
Name = name;
}
public AuthorityGroup()
{
Id = Guid.NewGuid();
CreatedDate = DateTime.Now;
}
public Guid Id { get; set; }
public string? Name { get; set; }
public long Version { get; set; }
public DateTime CreatedDate { get; set; }
}

View File

@ -0,0 +1,19 @@
namespace DotBased.AspNet.Authority.Models.Authority;
public abstract class AuthorityRole()
{
public AuthorityRole(string name) : this()
{
Name = name;
}
public Guid Id { get; set; } = Guid.NewGuid();
public string? Name { get; set; }
public long Version { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.Now;
public override string ToString() => Name ?? string.Empty;
}

View File

@ -0,0 +1,45 @@
using DotBased.AspNet.Authority.Attributes;
namespace DotBased.AspNet.Authority.Models.Authority;
public class AuthorityUser()
{
public AuthorityUser(string userName) : this()
{
UserName = userName;
}
public Guid Id { get; set; } = Guid.NewGuid();
public bool Enabled { get; set; }
public bool Confirmed { get; set; }
public bool Locked { get; set; }
public DateTime LockedDate { get; set; }
public string? UserName { get; set; }
public string? PasswordHash { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.Now;
public bool TwoFactorEnabled { get; set; }
public long Version { get; set; }
public long SecurityVersion { get; set; }
[Protect]
public string? EmailAddress { get; set; }
public bool EmailConfirmed { get; set; }
[Protect]
public string? PhoneNumber { get; set; }
public bool PhoneNumberConfirmed { get; set; }
public override string ToString() => UserName ?? EmailAddress ?? string.Empty;
}

View File

@ -0,0 +1,38 @@
using DotBased.AspNet.Authority.Models.Validation;
namespace DotBased.AspNet.Authority.Models;
public class AuthorityResult<TResultValue>
{
public AuthorityResult(bool success, string errorMessage = "", TResultValue? value = default, ResultFailReason reason = ResultFailReason.None, List<ValidationError>? errors = null)
{
Success = success;
ErrorMessage = errorMessage;
Value = value;
Reason = reason;
ValidationErrors = errors;
}
public bool Success { get; }
public string ErrorMessage { get; }
public TResultValue? Value { get; }
public ResultFailReason Reason { get; }
public List<ValidationError>? ValidationErrors { get; }
public static AuthorityResult<TResultValue> Ok(TResultValue? value) => new AuthorityResult<TResultValue>(true, value:value);
public static AuthorityResult<TResultValue> Error(string errorMessage, ResultFailReason reason = ResultFailReason.Error) =>
new AuthorityResult<TResultValue>(false, errorMessage, reason:reason);
public static AuthorityResult<TResultValue> Failed(List<ValidationError> errors, ResultFailReason reason = ResultFailReason.None)
=> new AuthorityResult<TResultValue>(false, errors:errors, reason:reason);
}
public enum ResultFailReason
{
None,
Unknown,
Validation,
Error
}

View File

@ -0,0 +1,11 @@
namespace DotBased.AspNet.Authority.Models.Options;
public class AuthorityOptions
{
public LockdownOptions Lockdown { get; set; } = new();
public LockoutOptions Lockout { get; set; } = new();
public PasswordOptions Password { get; set; } = new();
public ProviderOptions Provider { get; set; } = new();
public RepositoryOptions Repository { get; set; } = new();
public UserOptions User { get; set; } = new();
}

View File

@ -0,0 +1,7 @@
namespace DotBased.AspNet.Authority.Models.Options;
public enum ListOption
{
Blacklist,
Whitelist
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Models.Options;
public class LockdownOptions
{
public bool EnableLockout { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace DotBased.AspNet.Authority.Models.Options;
public class LockoutOptions
{
public bool EnableLockout { get; set; } = true;
public int FailedAttempts { get; set; } = 3;
public TimeSpan LockoutTimeout { get; set; } = TimeSpan.FromMinutes(30);
}

View File

@ -0,0 +1,14 @@
namespace DotBased.AspNet.Authority.Models.Options;
public class PasswordOptions
{
public int RequiredLength { get; set; } = 10;
public int MinimalUniqueChars { get; set; } = 1;
public bool RequireLowercase { get; set; }
public bool RequireUppercase { get; set; }
public bool RequireDigit { get; set; }
public bool RequireNonAlphanumeric { get; set; }
public List<string> PasswordBlackList { get; set; } = ["password", "1234"];
public StringComparer PasswordBlackListComparer { get; set; } = StringComparer.OrdinalIgnoreCase;
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Models.Options;
public class ProviderOptions
{
}

View File

@ -0,0 +1,10 @@
namespace DotBased.AspNet.Authority.Models.Options;
public class RepositoryOptions
{
/// <summary>
/// Use data encryption when a property has the <see cref="DotBased.AspNet.Authority.Attributes.ProtectAttribute"/> defined.
/// <value>Default: true</value>
/// </summary>
public bool UseDataProtection { get; set; } = true;
}

View File

@ -0,0 +1,8 @@
namespace DotBased.AspNet.Authority.Models.Options;
public class SignInOptions
{
public bool RequireVerifiedEmail { get; set; }
public bool RequireVerifiedPhoneNumber { get; set; }
public bool RequireConfirmedAccount { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace DotBased.AspNet.Authority.Models.Options;
public class UserOptions
{
public bool EnableRegister { get; set; }
public bool RequireUniqueEmail { get; set; }
public string UserNameCharacters { get; set; } = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@";
public ListOption UserNameCharacterListType { get; set; } = ListOption.Whitelist;
public List<string> UserNameBlackList { get; set; } = ["admin", "administrator", "dev", "developer"];
public StringComparer UserNameBlackListComparer { get; set; } = StringComparer.OrdinalIgnoreCase;
}

View File

@ -0,0 +1,24 @@
namespace DotBased.AspNet.Authority.Models.Validation;
public class ValidationError
{
public ValidationError(string validator, string errorCode, string description)
{
Validator = validator;
ErrorCode = errorCode;
Description = description;
}
/// <summary>
/// The validator name that generated this error.
/// </summary>
public string Validator { get; }
/// <summary>
/// The error code
/// </summary>
public string ErrorCode { get; }
/// <summary>
/// Error description
/// </summary>
public string Description { get; }
}

View File

@ -0,0 +1,21 @@
namespace DotBased.AspNet.Authority.Models.Validation;
public class ValidationResult
{
public ValidationResult(bool success, IEnumerable<ValidationError>? errors = null)
{
if (errors != null)
{
Errors = errors.ToList();
}
Success = success;
}
public bool Success { get; }
public IReadOnlyList<ValidationError> Errors { get; } = [];
public static ValidationResult Failed(IEnumerable<ValidationError> errors) => new(false, errors);
public static ValidationResult Ok() => new(true);
public override string ToString() => Success ? "Success" : $"Failed ({Errors.Count} errors)";
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Repositories;
public interface IAttributeRepository
{
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Repositories;
public interface IGroupRepository
{
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Repositories;
public interface IRoleRepository
{
}

View File

@ -0,0 +1,18 @@
using DotBased.AspNet.Authority.Models.Authority;
namespace DotBased.AspNet.Authority.Repositories;
public interface IUserRepository
{
public Task<AuthorityUser?> GetAuthorityUserByIdAsync(string id, CancellationToken? cancellationToken = null);
public Task<string> GetAuthorityUserIdAsync(AuthorityUser user, CancellationToken? cancellationToken = null);
public Task<Tuple<List<AuthorityUser>?, int>> GetAuthorityUsersAsync(string query, int maxResults = 20, int offset = 0, CancellationToken? cancellationToken = null);
public Task<AuthorityUser?> GetAuthorityUserByEmailAsync(string email, CancellationToken? cancellationToken = null);
public Task SetVersionAsync(AuthorityUser user, long version, CancellationToken? cancellationToken = null);
public Task<long> GetVersionAsync(AuthorityUser user, CancellationToken? cancellationToken = null);
public Task SetSecurityVersionAsync(AuthorityUser user, long version, CancellationToken? cancellationToken = null);
public Task<long> GetSecurityVersionAsync(AuthorityUser user, CancellationToken? cancellationToken = null);
public Task<AuthorityUser?> CreateUserAsync(AuthorityUser user, CancellationToken? cancellationToken = null);
public Task<AuthorityUser?> UpdateUserAsync(AuthorityUser user, CancellationToken? cancellationToken = null);
public Task<bool> DeleteUserAsync(AuthorityUser user, CancellationToken? cancellationToken = null);
}

View File

@ -0,0 +1,10 @@
using DotBased.AspNet.Authority.Managers;
using DotBased.AspNet.Authority.Models.Authority;
using DotBased.AspNet.Authority.Models.Validation;
namespace DotBased.AspNet.Authority.Validators;
public interface IPasswordValidator
{
public Task<ValidationResult> ValidatePasswordAsync(AuthorityManager manager, AuthorityUser user, string password);
}

View File

@ -0,0 +1,10 @@
using DotBased.AspNet.Authority.Managers;
using DotBased.AspNet.Authority.Models.Authority;
using DotBased.AspNet.Authority.Models.Validation;
namespace DotBased.AspNet.Authority.Validators;
public interface IUserValidator
{
public Task<ValidationResult> ValidateUserAsync(AuthorityManager manager, AuthorityUser user);
}

View File

@ -0,0 +1,22 @@
using DotBased.AspNet.Authority.Managers;
using DotBased.AspNet.Authority.Models.Authority;
using DotBased.AspNet.Authority.Models.Validation;
namespace DotBased.AspNet.Authority.Validators;
public class PasswordEqualsValidator : IPasswordValidator
{
private const string ValidatorId = "Authority.Validator.Password.Equals";
private const string ValidationBase = "Authority.Validation.Password";
public async Task<ValidationResult> ValidatePasswordAsync(AuthorityManager userManager, AuthorityUser user, string password)
{
List<ValidationError> errors = [];
var hashedPassword = await userManager.PasswordHasher.HashPasswordAsync(password);
if (user.PasswordHash != null && user.PasswordHash.Equals(hashedPassword, StringComparison.Ordinal))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.InUse", "User uses this password already!"));
}
return errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok();
}
}

View File

@ -0,0 +1,66 @@
using DotBased.AspNet.Authority.Managers;
using DotBased.AspNet.Authority.Models.Authority;
using DotBased.AspNet.Authority.Models.Validation;
using DotBased.Extensions;
namespace DotBased.AspNet.Authority.Validators;
/// <summary>
/// Validates the password against the options that is configured.
/// </summary>
public class PasswordOptionsValidator : IPasswordValidator
{
private const string ValidatorId = "Authority.Validator.Password.Options";
private const string ValidationBase = "Authority.Validation.Password";
public async Task<ValidationResult> ValidatePasswordAsync(AuthorityManager userManager, AuthorityUser user, string password)
{
if (userManager == null)
{
throw new ArgumentNullException(nameof(userManager), "User manager is not provided!");
}
var passwordOptions = userManager.Options.Password;
var errors = new List<ValidationError>();
if (password.IsNullOrEmpty() || password.Length < passwordOptions.RequiredLength)
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Required.Length", $"Password needs to have a minimum length of {passwordOptions.RequiredLength}"));
}
if (passwordOptions.RequireDigit && !ContainsDigit(password))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Required.Digit", "Password must contain a digit!"));
}
if (passwordOptions.RequireNonAlphanumeric && ContainsNonAlphanumeric(password))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Required.NonAlphanumeric", "Password must contain a non alphanumeric character."));
}
if (passwordOptions.RequireLowercase && password.Any(char.IsLower))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Required.Lowercase", "Password must contains at least one lowercase character."));
}
if (passwordOptions.RequireUppercase && password.Any(char.IsUpper))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Required.Uppercase", "Password must contains at least one uppercase character."));
}
if (passwordOptions.PasswordBlackList.Count != 0 && passwordOptions.PasswordBlackList.Contains(password, passwordOptions.PasswordBlackListComparer))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Blacklisted", "Given password is not allowed (blacklisted)"));
}
if (passwordOptions.MinimalUniqueChars > 0 && password.Distinct().Count() < passwordOptions.MinimalUniqueChars)
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.UniqueChars", $"Password must contain at least {passwordOptions.MinimalUniqueChars} unique chars."));
}
return await Task.FromResult(errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok());
}
private bool ContainsDigit(string strVal) => strVal.Any(char.IsDigit);
private bool ContainsNonAlphanumeric(string strVal) => !strVal.Any(char.IsLetterOrDigit);
}

View File

@ -0,0 +1,81 @@
using DotBased.AspNet.Authority.Managers;
using DotBased.AspNet.Authority.Models.Authority;
using DotBased.AspNet.Authority.Models.Options;
using DotBased.AspNet.Authority.Models.Validation;
namespace DotBased.AspNet.Authority.Validators;
public class UserValidator : IUserValidator
{
private const string ValidatorId = "Authority.Validator.User";
private const string ValidationBase = "Authority.Validation.User";
public async Task<ValidationResult> ValidateUserAsync(AuthorityManager manager, AuthorityUser user)
{
List<ValidationError> errors = [];
var userOptions = manager.Options.User;
if (userOptions.RequireUniqueEmail)
{
if (string.IsNullOrWhiteSpace(user.EmailAddress))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.NoEmail",
$"Option {nameof(UserOptions.RequireUniqueEmail)} is set to true but given user does not have an email address!"));
}
else
{
var userEmailResult = await manager.UserRepository.GetAuthorityUserByEmailAsync(user.EmailAddress);
if (userEmailResult != null)
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.EmailExists",
"Given email has already registered an account!"));
}
}
}
if (!string.IsNullOrWhiteSpace(user.UserName))
{
if (userOptions.UserNameBlackList.Count != 0 && userOptions.UserNameBlackList.Contains(user.UserName, userOptions.UserNameBlackListComparer))
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.Blacklisted", "Given username is not allowed (blacklisted)"));
}
if (!string.IsNullOrWhiteSpace(userOptions.UserNameCharacters))
{
List<char> chars = [];
if (userOptions.UserNameCharacterListType == ListOption.Whitelist)
{
chars.AddRange(user.UserName.Where(userNameChar => !userOptions.UserNameCharacters.Contains(userNameChar)));
}
if (userOptions.UserNameCharacterListType == ListOption.Blacklist)
{
chars.AddRange(user.UserName.Where(userNameChar => userOptions.UserNameCharacters.Contains(userNameChar)));
}
if (chars.Count <= 0) return errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok();
var errorCode = "";
var description = "";
switch (userOptions.UserNameCharacterListType)
{
case ListOption.Whitelist:
errorCode = "CharactersNotOnWhitelist";
description = $"Found characters in username that were not on the whitelist! Chars: [{string.Join(',', chars)}]";
break;
case ListOption.Blacklist:
errorCode = "CharactersOnBlacklist";
description = $"Found characters in username that are on the blacklist! Chars: [{string.Join(',', chars)}]";
break;
}
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.UserName.{errorCode}", description));
}
}
else
{
errors.Add(new ValidationError(ValidatorId, $"{ValidationBase}.InvalidUserName", "No username given!"));
}
return errors.Count > 0 ? ValidationResult.Failed(errors) : ValidationResult.Ok();
}
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Verifiers;
public interface IEmailVerifier
{
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Verifiers;
public interface IPhoneNumberVerifier
{
}

View File

@ -0,0 +1,6 @@
namespace DotBased.AspNet.Authority.Verifiers;
public interface IUserVerifier
{
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DotBased\DotBased.csproj" />
</ItemGroup>
</Project>

0
DotBased.Logging.MEL/BasedLogger.cs Normal file → Executable file
View File

0
DotBased.Logging.MEL/BasedLoggerProvider.cs Normal file → Executable file
View File

0
DotBased.Logging.MEL/DotBased.Logging.MEL.csproj Normal file → Executable file
View File

0
DotBased.Logging.MEL/LoggerBuilderExtensions.cs Normal file → Executable file
View File

0
DotBased.Logging.Serilog/BasedSerilog.cs Normal file → Executable file
View File

0
DotBased.Logging.Serilog/BasedSerilogEnricher.cs Normal file → Executable file
View File

View File

@ -20,7 +20,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazor.Wasm", "Blazor.Wasm\
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNet", "AspNet", "{624E7B11-8A18-46E5-AB1F-6AF6097F9D4D}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNet", "AspNet", "{624E7B11-8A18-46E5-AB1F-6AF6097F9D4D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.AspNet.Auth", "DotBased.AspNet.Auth\DotBased.AspNet.Auth.csproj", "{6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.AspNet.Authority", "DotBased.AspNet.Authority\DotBased.AspNet.Authority.csproj", "{A3ADC9AF-39B7-4EC4-8022-946118A8C322}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.Data", "DotBased.Data\DotBased.Data.csproj", "{2DF9FEEF-5A60-4B41-9B5F-F883DCE33EF4}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -56,10 +58,14 @@ Global
{AC8343A5-7953-4E1D-A926-406BE4D7E819}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC8343A5-7953-4E1D-A926-406BE4D7E819}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC8343A5-7953-4E1D-A926-406BE4D7E819}.Release|Any CPU.ActiveCfg = Release|Any CPU {AC8343A5-7953-4E1D-A926-406BE4D7E819}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC8343A5-7953-4E1D-A926-406BE4D7E819}.Release|Any CPU.Build.0 = Release|Any CPU {AC8343A5-7953-4E1D-A926-406BE4D7E819}.Release|Any CPU.Build.0 = Release|Any CPU
{6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A3ADC9AF-39B7-4EC4-8022-946118A8C322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B}.Debug|Any CPU.Build.0 = Debug|Any CPU {A3ADC9AF-39B7-4EC4-8022-946118A8C322}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3ADC9AF-39B7-4EC4-8022-946118A8C322}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B}.Release|Any CPU.Build.0 = Release|Any CPU {A3ADC9AF-39B7-4EC4-8022-946118A8C322}.Release|Any CPU.Build.0 = Release|Any CPU
{2DF9FEEF-5A60-4B41-9B5F-F883DCE33EF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2DF9FEEF-5A60-4B41-9B5F-F883DCE33EF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2DF9FEEF-5A60-4B41-9B5F-F883DCE33EF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2DF9FEEF-5A60-4B41-9B5F-F883DCE33EF4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC} = {2156FB93-C252-4B33-8A0C-73C82FABB163} {EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC} = {2156FB93-C252-4B33-8A0C-73C82FABB163}
@ -68,6 +74,7 @@ Global
{BADA4BAF-142B-47A8-95FC-B25E1D3D0020} = {DBDB4538-85D4-45AC-9E0A-A684467AEABA} {BADA4BAF-142B-47A8-95FC-B25E1D3D0020} = {DBDB4538-85D4-45AC-9E0A-A684467AEABA}
{AC8343A5-7953-4E1D-A926-406BE4D7E819} = {DBDB4538-85D4-45AC-9E0A-A684467AEABA} {AC8343A5-7953-4E1D-A926-406BE4D7E819} = {DBDB4538-85D4-45AC-9E0A-A684467AEABA}
{624E7B11-8A18-46E5-AB1F-6AF6097F9D4D} = {2156FB93-C252-4B33-8A0C-73C82FABB163} {624E7B11-8A18-46E5-AB1F-6AF6097F9D4D} = {2156FB93-C252-4B33-8A0C-73C82FABB163}
{6F407D81-DFAC-4936-ACDD-D75E9FDE2E7B} = {624E7B11-8A18-46E5-AB1F-6AF6097F9D4D} {A3ADC9AF-39B7-4EC4-8022-946118A8C322} = {624E7B11-8A18-46E5-AB1F-6AF6097F9D4D}
{2DF9FEEF-5A60-4B41-9B5F-F883DCE33EF4} = {2156FB93-C252-4B33-8A0C-73C82FABB163}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

0
DotBased/Objects/DbObjectAttribute.cs Normal file → Executable file
View File

0
DotBased/Objects/IObjectAttribute.cs Normal file → Executable file
View File

0
DotBased/Objects/ObjectAttribute.cs Normal file → Executable file
View File

Some files were not shown because too many files have changed in this diff Show More