From 427b924759b56a1bd429293d7a1ee4f88c3ecf71 Mon Sep 17 00:00:00 2001 From: Andre Beging Date: Mon, 4 Apr 2022 10:04:19 +0200 Subject: [PATCH] Auth System --- FoodsharingSiegen.Contracts/StorageKeys.cs | 13 ++ FoodsharingSiegen.Server/App.razor | 3 +- FoodsharingSiegen.Server/Auth/AuthHelper.cs | 133 ++++++++++++++++ FoodsharingSiegen.Server/Auth/AuthService.cs | 147 ++++++++++++++++++ .../Auth/LocalStorageService.cs | 78 ++++++++++ .../Auth/TokenAuthStateProvider.cs | 79 ++++++++++ FoodsharingSiegen.Server/Pages/Login.razor | 29 ++++ FoodsharingSiegen.Server/Pages/Logout.razor | 17 ++ FoodsharingSiegen.Server/Pages/_Host.cshtml | 2 +- FoodsharingSiegen.Server/Program.cs | 7 + .../Shared/LoginLayout.razor | 11 ++ .../Shared/MainLayout.razor | 33 ++-- FoodsharingSiegen.Server/Shared/NavMenu.razor | 6 +- .../Shared/RedirectToLogin.razor | 10 ++ 14 files changed, 552 insertions(+), 16 deletions(-) create mode 100644 FoodsharingSiegen.Contracts/StorageKeys.cs create mode 100644 FoodsharingSiegen.Server/Auth/AuthHelper.cs create mode 100644 FoodsharingSiegen.Server/Auth/AuthService.cs create mode 100644 FoodsharingSiegen.Server/Auth/LocalStorageService.cs create mode 100644 FoodsharingSiegen.Server/Auth/TokenAuthStateProvider.cs create mode 100644 FoodsharingSiegen.Server/Pages/Login.razor create mode 100644 FoodsharingSiegen.Server/Pages/Logout.razor create mode 100644 FoodsharingSiegen.Server/Shared/LoginLayout.razor create mode 100644 FoodsharingSiegen.Server/Shared/RedirectToLogin.razor diff --git a/FoodsharingSiegen.Contracts/StorageKeys.cs b/FoodsharingSiegen.Contracts/StorageKeys.cs new file mode 100644 index 0000000..dc09f46 --- /dev/null +++ b/FoodsharingSiegen.Contracts/StorageKeys.cs @@ -0,0 +1,13 @@ +namespace FoodsharingSiegen.Contracts +{ + /// + /// Sammlung von StorageKeys (Browser LocalStorage) + /// + public static class StorageKeys + { + /// + /// The token key + /// + public const string TokenKey = "_token"; + } +} \ No newline at end of file diff --git a/FoodsharingSiegen.Server/App.razor b/FoodsharingSiegen.Server/App.razor index c7730d1..a2fa46f 100644 --- a/FoodsharingSiegen.Server/App.razor +++ b/FoodsharingSiegen.Server/App.razor @@ -1,6 +1,7 @@  - + + diff --git a/FoodsharingSiegen.Server/Auth/AuthHelper.cs b/FoodsharingSiegen.Server/Auth/AuthHelper.cs new file mode 100644 index 0000000..7390a65 --- /dev/null +++ b/FoodsharingSiegen.Server/Auth/AuthHelper.cs @@ -0,0 +1,133 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Cryptography; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace FoodsharingSiegen.Server.Auth +{ + /// + /// The auth helper class (a. beging, 04.04.2022) + /// + public static class AuthHelper + { + #region Public Method Decrypt + + /// + /// Decrypts the crypted text (a. beging, 04.04.2022) + /// + /// The crypted text + /// The string + public static string Decrypt(string cryptedText) + { + CreateAlgorithm(out var tripleDes); + + var toEncryptArray = Convert.FromBase64String(cryptedText); + + + + var cTransform = tripleDes.CreateDecryptor(); + var resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length); + + tripleDes.Clear(); + + return Encoding.UTF8.GetString(resultArray); + } + + #endregion + + #region Public Method Encrypt + + /// + /// Encrypts the plain text (a. beging, 04.04.2022) + /// + /// The plain text + /// The string + public static string Encrypt(string plainText) + { + CreateAlgorithm(out var tripleDes); + + var toEncryptArray = Encoding.UTF8.GetBytes(plainText ); + + + + var cTransform = tripleDes.CreateEncryptor(); + + var resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length); + + tripleDes.Clear(); + + return Convert.ToBase64String(resultArray, 0, resultArray.Length); + } + + #endregion + + #region Public Method GetSigningKey + + /// + /// Gets the signing key (a. beging, 04.04.2022) + /// + /// The security key + public static SecurityKey GetSigningKey() => new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SigningKey)); + + #endregion + + #region Public Method ValidateToken + + /// + /// Validates the token using the specified token (a. beging, 04.04.2022) + /// + /// The token + /// A task containing the bool + public static async Task ValidateToken(string? token) + { + try + { + var tokenHandler = new JwtSecurityTokenHandler(); + var result = await tokenHandler.ValidateTokenAsync(token, new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = GetSigningKey(), + + ValidateAudience = true, + ValidAudience = "FS-Siegen", + + ValidateIssuer = true, + ValidIssuer = "FS-Siegen" + }); + + return result.IsValid; + } + catch (Exception e) + { + return false; + } + } + + #endregion + + #region Private Method CreateAlgorithm + + /// + /// Creates the algorithm using the specified triple des (a. beging, 04.04.2022) + /// + /// The triple des + private static void CreateAlgorithm(out TripleDES tripleDes) + { + var md5 = MD5.Create(); + var keyArray = md5.ComputeHash(Encoding.UTF8.GetBytes(SigningKey)); + md5.Clear(); + + tripleDes = TripleDES.Create(); + tripleDes.Key = keyArray; + tripleDes.Mode = CipherMode.ECB; + tripleDes.Padding = PaddingMode.PKCS7; + } + + #endregion + + /// + /// The signing key + /// + public const string SigningKey = "2uasw2§$%1nd47n9s43&%Zs3529s23&/%AW"; + } +} \ No newline at end of file diff --git a/FoodsharingSiegen.Server/Auth/AuthService.cs b/FoodsharingSiegen.Server/Auth/AuthService.cs new file mode 100644 index 0000000..3c47524 --- /dev/null +++ b/FoodsharingSiegen.Server/Auth/AuthService.cs @@ -0,0 +1,147 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using FoodsharingSiegen.Contracts; +using FoodsharingSiegen.Contracts.Entity; +using FoodsharingSiegen.Server.Data; +using FoodsharingSiegen.Server.Data.Service; +using FoodsharingSiegen.Server.Service; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; + +namespace FoodsharingSiegen.Server.Auth +{ + /// + /// The auth service class (a. beging, 04.04.2022) + /// + /// + public class AuthService : ServiceBase + { + #region Public Properties + + /// + /// Gets or sets the value of the user (ab) + /// + public User? User { get; set; } + + #endregion + + #region Private Fields + + /// + /// The authentication state provider + /// + private readonly AuthenticationStateProvider _authenticationStateProvider; + + /// + /// The local storage service + /// + private readonly LocalStorageService _localStorageService; + + #endregion + + #region Setup/Teardown + + /// + /// Initializes a new instance of the class + /// + /// The context + /// The local storage service + /// The authentication state provider + public AuthService(FsContext context, LocalStorageService localStorageService, AuthenticationStateProvider authenticationStateProvider) : base(context) + { + _localStorageService = localStorageService; + _authenticationStateProvider = authenticationStateProvider; + } + + #endregion + + #region Public Method Login + + /// + /// Logins the mail address (a. beging, 04.04.2022) + /// + /// The mail address + /// The password + /// A task containing the operation result + public async Task Login(string mailAddress, string password) + { + #region Ensure Admin + + var existingTroogS = await Context.Users.AnyAsync(x => x.Mail == "fs@beging.de"); + if (!existingTroogS) + { + var troogs = new User + { + Name = "Andre", + Mail = "fs@beging.de", + Type = UserType.Admin, + Created = DateTime.UtcNow, + EncryptedPassword = "qSIxTZo7J8M=" + }; + + await Context.Users.AddAsync(troogs); + await Context.SaveChangesAsync(); + } + + #endregion Ensure Admin + + var encryptedPassword = AuthHelper.Encrypt(password); + + User = await Context.Users.FirstOrDefaultAsync(x => x.Mail.ToLower() == mailAddress.ToLower() && x.EncryptedPassword == encryptedPassword); + + if (User != null) + { + + + // Daten korrekt + var tokenHandler = new JwtSecurityTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.NameIdentifier, User.Id.ToString()), + }), + Expires = DateTime.UtcNow.AddDays(30), + Issuer = "FS-Siegen", + Audience = "FS-Siegen", + SigningCredentials = new SigningCredentials(AuthHelper.GetSigningKey(), SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + var serializedToken = tokenHandler.WriteToken(token); + + await _localStorageService.SetItem(StorageKeys.TokenKey, serializedToken); + + return new OperationResult(); + } + + return new OperationResult(new Exception("Invalid")); + } + + #endregion + + #region Public Method Logout + + /// + /// Logouts this instance (a. beging, 04.04.2022) + /// + /// A task containing the operation result + public async Task Logout() + { + try + { + await _localStorageService.RemoveItem(StorageKeys.TokenKey); + User = null; + ((TokenAuthStateProvider) _authenticationStateProvider).MarkUserAsLoggedOut(); + return new OperationResult(); + } + catch (Exception e) + { + return new OperationResult(e); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/FoodsharingSiegen.Server/Auth/LocalStorageService.cs b/FoodsharingSiegen.Server/Auth/LocalStorageService.cs new file mode 100644 index 0000000..051e0be --- /dev/null +++ b/FoodsharingSiegen.Server/Auth/LocalStorageService.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using Microsoft.JSInterop; + +namespace FoodsharingSiegen.Server.Service +{ + /// + /// The local storage service class (a. beging, 02.04.2022) + /// + public class LocalStorageService + { + #region Private Fields + + /// + /// The js runtime + /// + private readonly IJSRuntime _jsRuntime; + + #endregion + + #region Setup/Teardown + + /// + /// Constructor + /// + /// + public LocalStorageService(IJSRuntime jsRuntime) => _jsRuntime = jsRuntime; + + #endregion + + #region Public Method GetItem + + /// + /// Ein Item aus dem LocalStorage laden + /// + /// Der Key des Items + /// Typ des Item + /// + public async Task GetItem(string key) + { + var json = await _jsRuntime.InvokeAsync("localStorage.getItem", key); + + if (json == null) + return default; + + return JsonSerializer.Deserialize(json); + } + + #endregion + + #region Public Method RemoveItem + + /// + /// Ein Item aus dem LocalStorage löschen + /// + /// Der Key des Items + public async Task RemoveItem(string key) + { + await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key); + } + + #endregion + + #region Public Method SetItem + + /// + /// Ein Item in den LocalStorage schreiben + /// + /// Der Key des Items + /// Das Item + /// Typ des Item + public async Task SetItem(string key, T value) + { + await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, JsonSerializer.Serialize(value)); + } + + #endregion + } +} \ No newline at end of file diff --git a/FoodsharingSiegen.Server/Auth/TokenAuthStateProvider.cs b/FoodsharingSiegen.Server/Auth/TokenAuthStateProvider.cs new file mode 100644 index 0000000..5e62014 --- /dev/null +++ b/FoodsharingSiegen.Server/Auth/TokenAuthStateProvider.cs @@ -0,0 +1,79 @@ +using System.Security.Claims; +using FoodsharingSiegen.Contracts; +using FoodsharingSiegen.Server.Auth; +using Microsoft.AspNetCore.Components.Authorization; + +namespace FoodsharingSiegen.Server.Service +{ + /// + /// The token auth state provider class (a. beging, 02.04.2022) + /// + /// + public class TokenAuthStateProvider : AuthenticationStateProvider + { + #region Private Fields + + /// LocalStorageService + private readonly LocalStorageService _localStorageService; + + #endregion + + #region Setup/Teardown + + /// + /// Constructor + /// + /// + public TokenAuthStateProvider(LocalStorageService localStorageService) => _localStorageService = localStorageService; + + #endregion + + #region Override GetAuthenticationStateAsync + + //////////////////////////////////////////////////////////////////////////////////////////////////// + /// Get the current authenticationstate + /// A. Beging, 02.02.2022. + //////////////////////////////////////////////////////////////////////////////////////////////////// + public override async Task GetAuthenticationStateAsync() + { + var token = await _localStorageService.GetItem(StorageKeys.TokenKey); + var tokenValid = await AuthHelper.ValidateToken(token); + + var identity = new ClaimsIdentity(); + if (tokenValid) + identity = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.Name, "user") + }, "TODO"); + + var claimsPrincipal = new ClaimsPrincipal(identity); + return new AuthenticationState(claimsPrincipal); + } + + #endregion + + #region Public Method MarkUserAsAuthenticated + + //////////////////////////////////////////////////////////////////////////////////////////////////// + /// Mark user as authenticated. + /// A. Beging, 02.02.2022. + //////////////////////////////////////////////////////////////////////////////////////////////////// + public void MarkUserAsAuthenticated() => NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); + + #endregion + + #region Public Method MarkUserAsLoggedOut + + /// + /// Marks the user as logged out (a. beging, 02.04.2022) + /// + public void MarkUserAsLoggedOut() + { + var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity()); + var authState = Task.FromResult(new AuthenticationState(anonymousUser)); + NotifyAuthenticationStateChanged(authState); + } + + #endregion + } +} \ No newline at end of file diff --git a/FoodsharingSiegen.Server/Pages/Login.razor b/FoodsharingSiegen.Server/Pages/Login.razor new file mode 100644 index 0000000..36d720c --- /dev/null +++ b/FoodsharingSiegen.Server/Pages/Login.razor @@ -0,0 +1,29 @@ +@page "/login" +@using FoodsharingSiegen.Server.Service +@using FoodsharingSiegen.Server.Auth +@layout LoginLayout + +@inject AuthService AuthService +@inject NavigationManager NavigationManager + +@code +{ + private string? Mailaddress { get; set; } + + private string? Password { get; set; } + + private async Task PerformLogin() + { + //Todo Eingaben Validieren [04.04.22 - Andre Beging] + + var loginR = await AuthService.Login(Mailaddress, Password); + if (loginR.Success) + { + NavigationManager.NavigateTo("/", true); + } + } +} + + + + \ No newline at end of file diff --git a/FoodsharingSiegen.Server/Pages/Logout.razor b/FoodsharingSiegen.Server/Pages/Logout.razor new file mode 100644 index 0000000..46ed21b --- /dev/null +++ b/FoodsharingSiegen.Server/Pages/Logout.razor @@ -0,0 +1,17 @@ +@layout LoginLayout +@page "/logout" +@using FoodsharingSiegen.Server.Service +@using FoodsharingSiegen.Server.Auth + +@inject AuthService AuthService +@inject NavigationManager NavigationManager + +@code { + + protected override async Task OnInitializedAsync() + { + var logoutR = await AuthService.Logout(); + if(logoutR.Success) NavigationManager.NavigateTo("/"); + } + +} \ No newline at end of file diff --git a/FoodsharingSiegen.Server/Pages/_Host.cshtml b/FoodsharingSiegen.Server/Pages/_Host.cshtml index 66166f2..2ba754f 100644 --- a/FoodsharingSiegen.Server/Pages/_Host.cshtml +++ b/FoodsharingSiegen.Server/Pages/_Host.cshtml @@ -5,4 +5,4 @@ Layout = "_Layout"; } - \ No newline at end of file + \ No newline at end of file diff --git a/FoodsharingSiegen.Server/Program.cs b/FoodsharingSiegen.Server/Program.cs index 8639da5..83e5ae5 100644 --- a/FoodsharingSiegen.Server/Program.cs +++ b/FoodsharingSiegen.Server/Program.cs @@ -1,8 +1,11 @@ using Blazorise; using Blazorise.Icons.Material; using Blazorise.Material; +using FoodsharingSiegen.Server.Auth; using FoodsharingSiegen.Server.Data; using FoodsharingSiegen.Server.Data.Service; +using FoodsharingSiegen.Server.Service; +using Microsoft.AspNetCore.Components.Authorization; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -15,7 +18,11 @@ builder.Services.AddDbContextFactory(opt => opt.UseSqlite($"Data Source=app.db")); // DI +builder.Services.AddScoped(); +builder.Services.AddScoped(); + builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/FoodsharingSiegen.Server/Shared/LoginLayout.razor b/FoodsharingSiegen.Server/Shared/LoginLayout.razor new file mode 100644 index 0000000..ed59518 --- /dev/null +++ b/FoodsharingSiegen.Server/Shared/LoginLayout.razor @@ -0,0 +1,11 @@ +@inherits LayoutComponentBase + +Login + +
+
+
+ @Body +
+
+
\ No newline at end of file diff --git a/FoodsharingSiegen.Server/Shared/MainLayout.razor b/FoodsharingSiegen.Server/Shared/MainLayout.razor index 1ff3c89..96922fd 100644 --- a/FoodsharingSiegen.Server/Shared/MainLayout.razor +++ b/FoodsharingSiegen.Server/Shared/MainLayout.razor @@ -2,18 +2,25 @@ FoodsharingSiegen.Server -
- + + +
+ -
-
- +
+
+ +
+ +
+ @Body +
+
- -
- @Body -
-
-
\ No newline at end of file +
+ + + +
\ No newline at end of file diff --git a/FoodsharingSiegen.Server/Shared/NavMenu.razor b/FoodsharingSiegen.Server/Shared/NavMenu.razor index 862a466..f83e9cf 100644 --- a/FoodsharingSiegen.Server/Shared/NavMenu.razor +++ b/FoodsharingSiegen.Server/Shared/NavMenu.razor @@ -19,9 +19,13 @@ Benutzer
+ - @code { private bool collapseNavMenu = true; diff --git a/FoodsharingSiegen.Server/Shared/RedirectToLogin.razor b/FoodsharingSiegen.Server/Shared/RedirectToLogin.razor new file mode 100644 index 0000000..f86f6c3 --- /dev/null +++ b/FoodsharingSiegen.Server/Shared/RedirectToLogin.razor @@ -0,0 +1,10 @@ +@inject NavigationManager NavigationManager + +@code +{ + protected override async Task OnInitializedAsync() + { + NavigationManager.NavigateTo("/login"); + await base.OnInitializedAsync(); + } +}