diff --git a/.gitea/workflows/BuildLibrary.yml b/.gitea/workflows/BuildLibrary.yml deleted file mode 100644 index f26a6ad..0000000 --- a/.gitea/workflows/BuildLibrary.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build C# Library - -run-name: Build Library project - -on: - push: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '8.0.x' - - - name: Restore dependencies - run: dotnet restore - - - name: Build library - run: dotnet build --configuration Release - - - name: Pack DotBased - run: dotnet pack ./DotBased/DotBased.csproj --configuration Release -o ./artifacts - - - name: Pack DotBased.Logging.MEL - run: dotnet pack ./DotBased.Logging.MEL/DotBased.Logging.MEL.csproj --configuration Release -o ./artifacts - - - name: Pack DotBased.Logging.Serilog - run: dotnet pack ./DotBased.Logging.Serilog/DotBased.Logging.Serilog.csproj --configuration Release -o ./artifacts - - - name: Publish library to Gitea NuGet - run: | - dotnet nuget add source \ - --username $GITEA_USER \ - --password $GITEA_TOKEN \ - --store-password-in-clear-text \ - --name gitea \ - https://git.netzbyte.com/api/packages/$GITEA_USER/nuget/index.json - - dotnet nuget push ./artifacts/*.nupkg --source gitea --skip-duplicate - env: - GITEA_USER: max - GITEA_TOKEN: ${{ secrets.PACKAGES_TOKEN }} \ No newline at end of file diff --git a/DotBased.ASP.Auth/AuthDataCache.cs b/DotBased.ASP.Auth/AuthDataCache.cs new file mode 100755 index 0000000..e8a1c18 --- /dev/null +++ b/DotBased.ASP.Auth/AuthDataCache.cs @@ -0,0 +1,98 @@ +using System.Collections.ObjectModel; +using DotBased.ASP.Auth.Domains.Auth; +using Microsoft.AspNetCore.Components.Authorization; + +namespace DotBased.ASP.Auth; + +public class AuthDataCache +{ + public AuthDataCache(BasedAuthConfiguration configuration) + { + _configuration = configuration; + } + + private readonly BasedAuthConfiguration _configuration; + + private readonly AuthStateCacheCollection _authenticationStateCollection = []; + + public ResultOld PurgeSessionState(string id) => _authenticationStateCollection.Remove(id) ? ResultOld.Ok() : ResultOld.Failed("Failed to purge session state from cache! Or the session was not cached..."); + + public void CacheSessionState(AuthenticationStateModel stateModel, AuthenticationState? state = null) => _authenticationStateCollection[stateModel.Id] = + new AuthStateCacheNode(stateModel, state); + + public ResultOld> RequestSessionState(string id) + { + if (!_authenticationStateCollection.TryGetValue(id, out var node)) + return ResultOld>.Failed("No cached object found!"); + string failedMsg; + if (node.StateModel != null) + { + if (node.IsValidLifespan(_configuration.CachedAuthSessionLifespan)) + return ResultOld>.Ok(new Tuple(node.StateModel, node.State)); + failedMsg = $"Session has invalid lifespan, removing entry: [{id}] from cache!"; + } + else + failedMsg = $"Returned object is null, removing entry: [{id}] from cache!"; + _authenticationStateCollection.Remove(id); + return ResultOld>.Failed(failedMsg); + } +} + +public class AuthStateCacheNode where TStateModel : class where TState : class +{ + public AuthStateCacheNode(TStateModel stateModel, TState? state) + { + StateModel = stateModel; + State = state; + } + public TStateModel? StateModel { get; private set; } + public TState? State { get; private set; } + public DateTime DateCached { get; private set; } = DateTime.Now; + + public void UpdateObject(TStateModel obj) + { + StateModel = obj; + DateCached = DateTime.Now; + } + + /// + /// Checks if the cached object is within the given lifespan. + /// + /// The max. lifespan + public bool IsValidLifespan(TimeSpan lifespan) => DateCached.Add(lifespan) > DateTime.Now; + + public override bool Equals(object? obj) + { + if (obj is AuthStateCacheNode cacheObj) + return StateModel != null && StateModel.Equals(cacheObj.StateModel); + return false; + } + + public override int GetHashCode() => typeof(TStateModel).GetHashCode(); + public override string ToString() => typeof(TStateModel).ToString(); +} + +public class AuthStateCacheCollection : KeyedCollection> where TStateModel : class where TState : class +{ + protected override string GetKeyForItem(AuthStateCacheNode item) => item.StateModel?.ToString() ?? string.Empty; + + public new AuthStateCacheNode? this[string id] + { + get => TryGetValue(id, out AuthStateCacheNode? nodeValue) ? nodeValue : null; + set + { + if (value == null) + return; + if (TryGetValue(id, out AuthStateCacheNode? nodeValue)) + Remove(nodeValue); + Add(value); + } + } + + public void Insert(AuthStateCacheNode node) + { + if (Contains(node)) + Remove(node); + Add(node); + } +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/AuthenticationService.cs b/DotBased.ASP.Auth/AuthenticationService.cs new file mode 100755 index 0000000..9b4d3b2 --- /dev/null +++ b/DotBased.ASP.Auth/AuthenticationService.cs @@ -0,0 +1,13 @@ +namespace DotBased.ASP.Auth.Services; + +public class AuthenticationService +{ + public AuthenticationService() + { + /* + * - Login + * - Logout + * - Register + */ + } +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/BasedAuthConfiguration.cs b/DotBased.ASP.Auth/BasedAuthConfiguration.cs new file mode 100755 index 0000000..a6871ba --- /dev/null +++ b/DotBased.ASP.Auth/BasedAuthConfiguration.cs @@ -0,0 +1,65 @@ +namespace DotBased.ASP.Auth; + +public class BasedAuthConfiguration +{ + /// + /// Allow users to registrate. + /// + public bool AllowRegistration { get; set; } + //TODO: Callback when a user registers, so the application can handle sending emails or generate a code to complete the registration. + //TODO: Callback for validation email, phone number + /// + /// Allow no passwords on users, not recommended! + /// + public bool AllowEmptyPassword { get; set; } = false; + /// + /// This path is used for redirecting to the login page. + /// + public string LoginPath { get; set; } = string.Empty; + /// + /// The path that will be used if the logout is requested. + /// + public string LogoutPath { get; set; } = string.Empty; + /// + /// The page that the client will be redirected to after logging out. + /// + public string LoggedOutPath { get; set; } = string.Empty; + /// + /// The max age before a AuthenticationState will expire (default: 7 days). + /// + public TimeSpan AuthenticationStateMaxAgeBeforeExpire { get; set; } = TimeSpan.FromDays(7); + /// + /// How long a session state will be cached (default: 15 min) + /// + public TimeSpan CachedAuthSessionLifespan { get; set; } = TimeSpan.FromMinutes(15); + /// + /// Can be used to seed a default user and/or group for first time use. + /// + public Action? SeedData { get; set; } + + public Type? AuthDataRepositoryType { get; private set; } + + public void SetDataRepositoryType() where TDataProviderType : IAuthDataRepository => + AuthDataRepositoryType = typeof(TDataProviderType); + + public Type? SessionStateProviderType { get; private set; } + + public void SetSessionStateProviderType() + where TSessionStateProviderType : ISessionStateProvider => + SessionStateProviderType = typeof(TSessionStateProviderType); +} + +public class BasedPasswordOptions +{ + +} + +public class BasedUserOptions +{ + +} + +public class BasedLockoutOptions +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/BasedAuthDefaults.cs b/DotBased.ASP.Auth/BasedAuthDefaults.cs new file mode 100755 index 0000000..824740b --- /dev/null +++ b/DotBased.ASP.Auth/BasedAuthDefaults.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +namespace DotBased.ASP.Auth; + +public static class BasedAuthDefaults +{ + public const string AuthenticationScheme = "DotBasedAuthentication"; + public const string StorageKey = "dotbased_session"; + + public static IComponentRenderMode InteractiveServerWithoutPrerender { get; } = + new InteractiveServerRenderMode(prerender: false); +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/BasedServerAuthenticationStateProvider.cs b/DotBased.ASP.Auth/BasedServerAuthenticationStateProvider.cs new file mode 100755 index 0000000..0863561 --- /dev/null +++ b/DotBased.ASP.Auth/BasedServerAuthenticationStateProvider.cs @@ -0,0 +1,40 @@ +using System.Security.Claims; +using DotBased.Logging; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; +using ILogger = DotBased.Logging.ILogger; + +namespace DotBased.ASP.Auth; + +// RevalidatingServerAuthenticationStateProvider +// AuthenticationStateProvider +// Handles roles +public class BasedServerAuthenticationStateProvider : ServerAuthenticationStateProvider +{ + public BasedServerAuthenticationStateProvider(BasedAuthConfiguration configuration, ProtectedLocalStorage localStorage, SecurityService securityService) + { + _config = configuration; + _localStorage = localStorage; + _securityService = securityService; + _logger = LogService.RegisterLogger(); + } + + private BasedAuthConfiguration _config; + private readonly ProtectedLocalStorage _localStorage; + private readonly SecurityService _securityService; + private readonly ILogger _logger; + private readonly AuthenticationState _anonState = new(new ClaimsPrincipal()); + + + public override async Task GetAuthenticationStateAsync() + { + _logger.Debug("Getting authentication state..."); + var sessionIdResult = await _localStorage.GetAsync(BasedAuthDefaults.StorageKey); + if (!sessionIdResult.Success || sessionIdResult.Value == null) + return _anonState; + _logger.Debug("Found state [{State}], getting session from {Service}", sessionIdResult.Value, nameof(SecurityService)); + var stateResult = await _securityService.GetAuthenticationStateFromSessionAsync(sessionIdResult.Value); + return stateResult is { Success: true, Value: not null } ? stateResult.Value : _anonState; + } +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Auth/AuthenticationStateModel.cs b/DotBased.ASP.Auth/Domains/Auth/AuthenticationStateModel.cs new file mode 100755 index 0000000..78a5c79 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Auth/AuthenticationStateModel.cs @@ -0,0 +1,25 @@ +using DotBased.ASP.Auth.Domains.Identity; + +namespace DotBased.ASP.Auth.Domains.Auth; + +public class AuthenticationStateModel +{ + public AuthenticationStateModel(UserModel user) + { + UserId = user.Id; + } + + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string UserId { get; set; } + public DateTime CreationDate { get; set; } = DateTime.Now; + + public override bool Equals(object? obj) + { + if (obj is AuthenticationStateModel authStateModel) + return authStateModel.Id == Id; + return false; + } + // ReSharper disable once NonReadonlyMemberInGetHashCode + public override int GetHashCode() => Id.GetHashCode(); + public override string ToString() => Id; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Auth/PermissionModel.cs b/DotBased.ASP.Auth/Domains/Auth/PermissionModel.cs new file mode 100755 index 0000000..93353fe --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Auth/PermissionModel.cs @@ -0,0 +1,8 @@ +namespace DotBased.ASP.Auth.Domains.Auth; + +public class PermissionModel +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Permission { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Auth/RoleModel.cs b/DotBased.ASP.Auth/Domains/Auth/RoleModel.cs new file mode 100755 index 0000000..92b15c7 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Auth/RoleModel.cs @@ -0,0 +1,12 @@ +using DotBased.Objects; + +namespace DotBased.ASP.Auth.Domains.Auth; + +public class RoleModel +{ + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List Permissions { get; set; } = []; + public List> Attributes { get; set; } = []; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Identity/GroupItemModel.cs b/DotBased.ASP.Auth/Domains/Identity/GroupItemModel.cs new file mode 100755 index 0000000..30508d8 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Identity/GroupItemModel.cs @@ -0,0 +1,8 @@ +namespace DotBased.ASP.Auth.Domains.Identity; + +public class GroupItemModel +{ + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Identity/GroupModel.cs b/DotBased.ASP.Auth/Domains/Identity/GroupModel.cs new file mode 100755 index 0000000..73ff377 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Identity/GroupModel.cs @@ -0,0 +1,13 @@ +using DotBased.ASP.Auth.Domains.Auth; +using DotBased.Objects; + +namespace DotBased.ASP.Auth.Domains.Identity; + +public class GroupModel +{ + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List Roles { get; set; } = []; + public List> Attributes { get; set; } = []; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Identity/UserItemModel.cs b/DotBased.ASP.Auth/Domains/Identity/UserItemModel.cs new file mode 100755 index 0000000..1d07566 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Identity/UserItemModel.cs @@ -0,0 +1,10 @@ +namespace DotBased.ASP.Auth.Domains.Identity; + +public class UserItemModel +{ + public string Id { get; set; } = string.Empty; + public string UserName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string FamilyName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/Identity/UserModel.cs b/DotBased.ASP.Auth/Domains/Identity/UserModel.cs new file mode 100755 index 0000000..b9a9f51 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/Identity/UserModel.cs @@ -0,0 +1,31 @@ +using DotBased.ASP.Auth.Domains.Auth; +using DotBased.Objects; + +namespace DotBased.ASP.Auth.Domains.Identity; + +public class UserModel +{ + public string UserName { get; set; } = string.Empty; + public string PasswordHash { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string PhoneNumber { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string FamilyName { get; set; } = string.Empty; + public DateTime Dob { get; set; } + + public string Id { get; set; } = Guid.NewGuid().ToString(); + public bool Enabled { get; set; } + public bool EmailValidated { get; set; } + public bool PhoneNumberConfirmed { get; set; } + public bool Lockout { get; set; } + public DateTime LockoutEnd { get; set; } + public DateTime CreationStamp { get; set; } + public DateTime SecurityStamp { get; set; } + public DateTime ConcurrencyStamp { get; set; } + public int AccessFailedCount { get; set; } + public bool ExternalAuthentication { get; set; } + + public List Groups { get; set; } = []; + public List Roles { get; set; } = []; + public List> Attributes { get; set; } = []; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/LoginModel.cs b/DotBased.ASP.Auth/Domains/LoginModel.cs new file mode 100755 index 0000000..a011eb6 --- /dev/null +++ b/DotBased.ASP.Auth/Domains/LoginModel.cs @@ -0,0 +1,8 @@ +namespace DotBased.ASP.Auth.Domains; + +public class LoginModel +{ + public string UserName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Domains/RegisterModel.cs b/DotBased.ASP.Auth/Domains/RegisterModel.cs new file mode 100755 index 0000000..877476c --- /dev/null +++ b/DotBased.ASP.Auth/Domains/RegisterModel.cs @@ -0,0 +1,10 @@ +namespace DotBased.ASP.Auth.Domains; + +public class RegisterModel +{ + public string UserName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string FamilyName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/DotBased.ASP.Auth.csproj b/DotBased.ASP.Auth/DotBased.ASP.Auth.csproj new file mode 100755 index 0000000..e51c97a --- /dev/null +++ b/DotBased.ASP.Auth/DotBased.ASP.Auth.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/DotBased.ASP.Auth/DotBasedAuthDependencyInjection.cs b/DotBased.ASP.Auth/DotBasedAuthDependencyInjection.cs new file mode 100755 index 0000000..b060926 --- /dev/null +++ b/DotBased.ASP.Auth/DotBasedAuthDependencyInjection.cs @@ -0,0 +1,55 @@ +using DotBased.ASP.Auth.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; + +namespace DotBased.ASP.Auth; + +public static class DotBasedAuthDependencyInjection +{ + /// + /// Use the DotBased authentication implementation + /// + /// Use UseBasedServerAuth()! + /// Service collection + /// DotBased auth configuration + public static IServiceCollection AddBasedServerAuth(this IServiceCollection services, Action? configurationAction = null) + { + var Configuration = new BasedAuthConfiguration(); + configurationAction?.Invoke(Configuration); + + services.AddSingleton(Configuration); + if (Configuration.AuthDataRepositoryType == null) + throw new ArgumentNullException(nameof(Configuration.AuthDataRepositoryType), $"No '{nameof(IAuthDataRepository)}' configured!"); + services.AddScoped(typeof(IAuthDataRepository), Configuration.AuthDataRepositoryType); + + services.AddSingleton(); + services.AddScoped(); + + services.AddScoped(); + services.AddAuthentication(options => + { + options.DefaultScheme = BasedAuthDefaults.AuthenticationScheme; + }); + services.AddAuthorization(); + services.AddCascadingAuthenticationState(); + return services; + } + + public static WebApplication UseBasedServerAuth(this WebApplication app) + { + app.UseAuthentication(); + app.UseAuthorization(); + + // Data + var authConfig = app.Services.GetService(); + if (authConfig == null) + throw new NullReferenceException($"{nameof(BasedAuthConfiguration)} is null!"); + if (authConfig.AuthDataRepositoryType == null) + throw new NullReferenceException($"{nameof(authConfig.AuthDataRepositoryType)} is null, cannot instantiate an instance of {nameof(IAuthDataRepository)}"); + var dataProvider = (IAuthDataRepository?)Activator.CreateInstance(authConfig.AuthDataRepositoryType); + if (dataProvider != null) authConfig.SeedData?.Invoke(dataProvider); + + return app; + } +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/IAuthDataRepository.cs b/DotBased.ASP.Auth/IAuthDataRepository.cs new file mode 100755 index 0000000..4435324 --- /dev/null +++ b/DotBased.ASP.Auth/IAuthDataRepository.cs @@ -0,0 +1,22 @@ +using DotBased.ASP.Auth.Domains.Auth; +using DotBased.ASP.Auth.Domains.Identity; + +namespace DotBased.ASP.Auth; + +public interface IAuthDataRepository +{ + public Task CreateUserAsync(UserModel user); + public Task UpdateUserAsync(UserModel user); + public Task DeleteUserAsync(UserModel user); + public Task> GetUserAsync(string id, string email, string username); + public Task> GetUsersAsync(int start = 0, int amount = 30, string search = ""); + public Task CreateGroupAsync(GroupModel group); + public Task UpdateGroupAsync(GroupModel group); + public Task DeleteGroupAsync(GroupModel group); + public Task> GetGroupAsync(string id); + public Task> GetGroupsAsync(int start = 0, int amount = 30, string search = ""); + public Task CreateAuthenticationStateAsync(AuthenticationStateModel authenticationState); + public Task UpdateAuthenticationStateAsync(AuthenticationStateModel authenticationState); + public Task DeleteAuthenticationStateAsync(AuthenticationStateModel authenticationState); + public Task> GetAuthenticationStateAsync(string id); +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/ISessionStateProvider.cs b/DotBased.ASP.Auth/ISessionStateProvider.cs new file mode 100755 index 0000000..794429c --- /dev/null +++ b/DotBased.ASP.Auth/ISessionStateProvider.cs @@ -0,0 +1,8 @@ +namespace DotBased.ASP.Auth; + +public interface ISessionStateProvider +{ + public const string SessionStateName = "BasedServerSession"; + public Task> GetSessionStateAsync(); + public Task SetSessionStateAsync(string state); +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/MemoryAuthDataRepository.cs b/DotBased.ASP.Auth/MemoryAuthDataRepository.cs new file mode 100755 index 0000000..a2914a7 --- /dev/null +++ b/DotBased.ASP.Auth/MemoryAuthDataRepository.cs @@ -0,0 +1,107 @@ +using System.Diagnostics.CodeAnalysis; +using DotBased.ASP.Auth.Domains.Auth; +using DotBased.ASP.Auth.Domains.Identity; +using DotBased.Extensions; + +namespace DotBased.ASP.Auth; +/// +/// In memory data provider, for testing only! +/// +[SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] +public class MemoryAuthDataRepository : IAuthDataRepository +{ + public async Task CreateUserAsync(UserModel user) + { + if (MemoryData.users.Any(x => x.Id == user.Id || x.Email == user.Email)) + return ResultOld.Failed("User already exists."); + MemoryData.users.Add(user); + return ResultOld.Ok(); + } + + public async Task UpdateUserAsync(UserModel user) + { + if (MemoryData.users.All(x => x.Id != user.Id)) + return ResultOld.Failed("User does not exist!"); + + return ResultOld.Ok(); + } + + public Task DeleteUserAsync(UserModel user) + { + throw new NotImplementedException(); + } + + public async Task> GetUserAsync(string id, string email, string username) + { + UserModel? userModel = null; + if (!id.IsNullOrEmpty()) + userModel = MemoryData.users.FirstOrDefault(u => u.Id.Equals(id, StringComparison.OrdinalIgnoreCase)); + if (!email.IsNullOrEmpty()) + userModel = MemoryData.users.FirstOrDefault(u => u.Email.Equals(email, StringComparison.OrdinalIgnoreCase)); + if (!username.IsNullOrEmpty()) + userModel = MemoryData.users.FirstOrDefault(u => u.UserName.Equals(username, StringComparison.OrdinalIgnoreCase)); + return userModel != null ? ResultOld.Ok(userModel) : ResultOld.Failed("No user found!"); + } + + public Task> GetUsersAsync(int start = 0, int amount = 30, string search = "") + { + throw new NotImplementedException(); + } + + public Task CreateGroupAsync(GroupModel group) + { + throw new NotImplementedException(); + } + + public Task UpdateGroupAsync(GroupModel group) + { + throw new NotImplementedException(); + } + + public Task DeleteGroupAsync(GroupModel group) + { + throw new NotImplementedException(); + } + + public Task> GetGroupAsync(string id) + { + throw new NotImplementedException(); + } + + public Task> GetGroupsAsync(int start = 0, int amount = 30, string search = "") + { + throw new NotImplementedException(); + } + + public async Task CreateAuthenticationStateAsync(AuthenticationStateModel authenticationState) + { + if (MemoryData.AuthenticationStates.Contains(authenticationState)) return ResultOld.Failed("Item already exists!"); + MemoryData.AuthenticationStates.Add(authenticationState); + return ResultOld.Ok(); + } + + public Task UpdateAuthenticationStateAsync(AuthenticationStateModel authenticationState) + { + throw new NotImplementedException(); + } + + public async Task DeleteAuthenticationStateAsync(AuthenticationStateModel authenticationState) + { + MemoryData.AuthenticationStates.Remove(authenticationState); + return ResultOld.Ok(); + } + + public async Task> GetAuthenticationStateAsync(string id) + { + var item = MemoryData.AuthenticationStates.FirstOrDefault(x => x.Id == id); + if (item == null) return ResultOld.Failed("Could not get the session state!"); + return ResultOld.Ok(item); + } +} + +internal static class MemoryData +{ + public static readonly List users = []; + public static readonly List Groups = []; + public static readonly List AuthenticationStates = []; +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/AuthConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/AuthConfiguration.cs new file mode 100755 index 0000000..2bbf690 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/AuthConfiguration.cs @@ -0,0 +1,11 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class AuthConfiguration +{ + public CacheConfiguration Cache { get; set; } = new(); + public LockoutConfiguration Lockout { get; set; } = new(); + public PasswordConfiguration Password { get; set; } = new(); + public ProviderConfiguration Provider { get; set; } = new(); + public RepositoryConfiguration Repository { get; set; } = new(); + public UserConfiguration User { get; set; } = new(); +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/CacheConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/CacheConfiguration.cs new file mode 100755 index 0000000..8647941 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/CacheConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class CacheConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/LockoutConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/LockoutConfiguration.cs new file mode 100755 index 0000000..b59ae65 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/LockoutConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class LockoutConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/PasswordConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/PasswordConfiguration.cs new file mode 100755 index 0000000..c590cdd --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/PasswordConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class PasswordConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/ProviderConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/ProviderConfiguration.cs new file mode 100755 index 0000000..cf3f702 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/ProviderConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class ProviderConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/RepositoryConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/RepositoryConfiguration.cs new file mode 100755 index 0000000..cb55903 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/RepositoryConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class RepositoryConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/Models/Configuration/UserConfiguration.cs b/DotBased.ASP.Auth/Models/Configuration/UserConfiguration.cs new file mode 100755 index 0000000..a4dd082 --- /dev/null +++ b/DotBased.ASP.Auth/Models/Configuration/UserConfiguration.cs @@ -0,0 +1,6 @@ +namespace DotBased.ASP.Auth.Models.Configuration; + +public class UserConfiguration +{ + +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/SecurityManager.cs b/DotBased.ASP.Auth/SecurityManager.cs new file mode 100755 index 0000000..0067eae --- /dev/null +++ b/DotBased.ASP.Auth/SecurityManager.cs @@ -0,0 +1,9 @@ +namespace DotBased.ASP.Auth.Managers; + +public class SecurityManager +{ + public SecurityManager() + { + + } +} \ No newline at end of file diff --git a/DotBased.ASP.Auth/SecurityService.cs b/DotBased.ASP.Auth/SecurityService.cs new file mode 100755 index 0000000..48e1c80 --- /dev/null +++ b/DotBased.ASP.Auth/SecurityService.cs @@ -0,0 +1,137 @@ +using System.Security.Claims; +using DotBased.ASP.Auth.Domains; +using DotBased.ASP.Auth.Domains.Auth; +using DotBased.ASP.Auth.Domains.Identity; +using DotBased.Extensions; +using DotBased.Logging; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; + +namespace DotBased.ASP.Auth; + +public class SecurityService +{ + public SecurityService(IAuthDataRepository authDataRepository, AuthDataCache dataCache, ProtectedLocalStorage localStorage) + { + _authDataRepository = authDataRepository; + _dataCache = dataCache; + _localStorage = localStorage; + _logger = LogService.RegisterLogger(); + } + + private readonly IAuthDataRepository _authDataRepository; + private readonly AuthDataCache _dataCache; + private readonly ProtectedLocalStorage _localStorage; + private readonly ILogger _logger; + + public async Task> GetAuthenticationStateFromSessionAsync(string id) + { + if (id.IsNullOrEmpty()) + return ResultOld.Failed("No valid id!"); + AuthenticationStateModel? authStateModel = null; + var stateCache = _dataCache.RequestSessionState(id); + if (!stateCache.Success || stateCache.Value == null) + { + var stateResult = await _authDataRepository.GetAuthenticationStateAsync(id); + if (stateResult is { Success: true, Value: not null }) + { + authStateModel = stateResult.Value; + _dataCache.CacheSessionState(authStateModel); + } + } + else + { + if (stateCache.Value.Item2 != null) + return ResultOld.Ok(stateCache.Value.Item2); + authStateModel = stateCache.Value.Item1; + } + + if (authStateModel == null) + return ResultOld.Failed("Failed to get auth state!"); + + var userResult = await _authDataRepository.GetUserAsync(authStateModel.UserId, string.Empty, string.Empty); + if (userResult is not { Success: true, Value: not null }) + return ResultOld.Failed("Failed to get user from state!"); + var claims = new List() + { + new(ClaimTypes.Sid, userResult.Value.Id), + new(ClaimTypes.Name, userResult.Value.Name), + new(ClaimTypes.NameIdentifier, userResult.Value.UserName), + new(ClaimTypes.Surname, userResult.Value.FamilyName), + new(ClaimTypes.Email, userResult.Value.Email) + }; + claims.AddRange(userResult.Value.Groups.Select(group => new Claim(ClaimTypes.GroupSid, group.Id))); + claims.AddRange(userResult.Value.Roles.Select(role => new Claim(ClaimTypes.Role, role.Name))); + claims.AddRange(userResult.Value.Groups.Select(g => g.Roles).SelectMany(gRolesList => gRolesList, (_, role) => new Claim(ClaimTypes.Role, role.Name))); + var claimsIdentity = new ClaimsIdentity(claims, BasedAuthDefaults.AuthenticationScheme); + var authState = new AuthenticationState(new ClaimsPrincipal(claimsIdentity)); + _dataCache.CacheSessionState(authStateModel, authState); + return ResultOld.Ok(authState); + } + + public async Task> LoginAsync(LoginModel login) + { + try + { + UserModel? user = null; + ResultOld usrResultOld; + if (!login.UserName.IsNullOrEmpty()) + { + usrResultOld = await _authDataRepository.GetUserAsync(string.Empty, string.Empty, login.UserName); + if (usrResultOld is { Success: true, Value: not null }) + user = usrResultOld.Value; + } + else if (!login.Email.IsNullOrEmpty()) + { + usrResultOld = await _authDataRepository.GetUserAsync(string.Empty, login.Email, string.Empty); + if (usrResultOld is { Success: true, Value: not null }) + user = usrResultOld.Value; + } + else + return ResultOld.Failed("Username & Email is empty, cannot login!"); + + if (user == null || !usrResultOld.Success) + return ResultOld.Failed("No user found!"); + + if (user.PasswordHash != login.Password) //TODO: Hash password and compare + return ResultOld.Failed("Login failed, invalid password."); + var state = new AuthenticationStateModel(user); + var authResult = await _authDataRepository.CreateAuthenticationStateAsync(state); + if (!authResult.Success) + return ResultOld.Failed("Failed to store session to database!"); + _dataCache.CacheSessionState(state); + await _localStorage.SetAsync(BasedAuthDefaults.StorageKey, state.Id); + return ResultOld.Ok(state); + } + catch (Exception e) + { + _logger.Error(e, "Failed to login!"); + return ResultOld.Failed("Login failed, exception thrown!"); + } + } + + public async Task LogoutAsync(string state) + { + try + { + if (state.IsNullOrEmpty()) + return ResultOld.Failed($"Argument {nameof(state)} is empty!"); + + var stateResult = await _authDataRepository.GetAuthenticationStateAsync(state); + if (!stateResult.Success || stateResult.Value == null) + return stateResult; + var authState = stateResult.Value; + + _dataCache.PurgeSessionState(state); + var updatedStateResult = await _authDataRepository.DeleteAuthenticationStateAsync(authState); + if (updatedStateResult.Success) return updatedStateResult; + _logger.Warning(updatedStateResult.Message); + return updatedStateResult; + } + catch (Exception e) + { + _logger.Error(e, "Failed to logout!"); + return ResultOld.Failed("Failed to logout, exception thrown!"); + } + } +} \ No newline at end of file diff --git a/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs b/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs index 5157eeb..16681d3 100755 --- a/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs +++ b/DotBased.AspNet.Authority/AuthorityProviderExtensions.cs @@ -26,7 +26,6 @@ public static class AuthorityProviderExtensions services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); - /*services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped();*/ diff --git a/DotBased.sln b/DotBased.sln index 484dcab..daa7f98 100755 --- a/DotBased.sln +++ b/DotBased.sln @@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.Logging.Serilog", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{2156FB93-C252-4B33-8A0C-73C82FABB163}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.ASP.Auth", "DotBased.ASP.Auth\DotBased.ASP.Auth.csproj", "{CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.Logging.MEL", "DotBased.Logging.MEL\DotBased.Logging.MEL.csproj", "{D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{DBDB4538-85D4-45AC-9E0A-A684467AEABA}" @@ -40,6 +42,10 @@ Global {EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC}.Release|Any CPU.Build.0 = Release|Any CPU + {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Release|Any CPU.Build.0 = Release|Any CPU {D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -63,6 +69,7 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC} = {2156FB93-C252-4B33-8A0C-73C82FABB163} + {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D} = {2156FB93-C252-4B33-8A0C-73C82FABB163} {D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D} = {2156FB93-C252-4B33-8A0C-73C82FABB163} {BADA4BAF-142B-47A8-95FC-B25E1D3D0020} = {DBDB4538-85D4-45AC-9E0A-A684467AEABA} {AC8343A5-7953-4E1D-A926-406BE4D7E819} = {DBDB4538-85D4-45AC-9E0A-A684467AEABA}