From 2c457db5ae9a3bfe7467e81187daa2807479318e Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 9 Aug 2025 22:02:00 +0200 Subject: [PATCH] better JWT creation and add Group based Authentication --- .../Repositories/GroupRepository.cs | 11 ++++- .../Repositories/UserRepository.cs | 13 ++++- .../Utils/BaseRepository.cs | 11 ++++- ApplicationHub.Data.EF/Utils/QueryResolver.cs | 49 +++++++++++++++++++ .../AuthenticationConfiguration.cs | 2 + .../Authentication/LoginController.cs | 43 ++++++---------- .../Controllers/Message/GreeterController.cs | 14 ++++-- ApplicationHub/Jwt/Attributes/HasGroup.cs | 16 ++++++ ApplicationHub/Jwt/Group.cs | 8 +++ ApplicationHub/Jwt/Services/JwtService.cs | 32 ++++++++++++ ApplicationHub/appsettings.Development.json | 2 +- ApplicationHub/appsettings.json | 2 +- 12 files changed, 168 insertions(+), 35 deletions(-) create mode 100644 ApplicationHub/Jwt/Attributes/HasGroup.cs create mode 100644 ApplicationHub/Jwt/Group.cs create mode 100644 ApplicationHub/Jwt/Services/JwtService.cs diff --git a/ApplicationHub.Data.EF/Authentication/Repositories/GroupRepository.cs b/ApplicationHub.Data.EF/Authentication/Repositories/GroupRepository.cs index 58f4bf9..2dc4833 100644 --- a/ApplicationHub.Data.EF/Authentication/Repositories/GroupRepository.cs +++ b/ApplicationHub.Data.EF/Authentication/Repositories/GroupRepository.cs @@ -2,7 +2,16 @@ using ApplicationHub.Data.EF.Utils; using ApplicationHub.Domain.Contracts.Authentication.Models; using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; namespace ApplicationHub.Data.EF.Authentication.Repositories; -public class GroupRepository(AuthenticationDataContext authenticationDataContext, IMapper mapper) : BaseRepository(authenticationDataContext.Groups, mapper); \ No newline at end of file +public class GroupRepository(AuthenticationDataContext authenticationDataContext, IMapper mapper) : BaseRepository(authenticationDataContext.Groups, mapper) +{ + protected override IIncludableQueryable? Inculdes() + { + return authenticationDataContext.Groups + .Include(x => x.Rights); + } +} \ No newline at end of file diff --git a/ApplicationHub.Data.EF/Authentication/Repositories/UserRepository.cs b/ApplicationHub.Data.EF/Authentication/Repositories/UserRepository.cs index 7efd598..f398f51 100644 --- a/ApplicationHub.Data.EF/Authentication/Repositories/UserRepository.cs +++ b/ApplicationHub.Data.EF/Authentication/Repositories/UserRepository.cs @@ -2,7 +2,18 @@ using ApplicationHub.Data.EF.Utils; using ApplicationHub.Domain.Contracts.Authentication.Models; using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; namespace ApplicationHub.Data.EF.Authentication.Repositories; -public class UserRepository(AuthenticationDataContext authenticationDataContext, IMapper mapper) : BaseRepository(authenticationDataContext.Users, mapper); \ No newline at end of file +public class UserRepository(AuthenticationDataContext authenticationDataContext, IMapper mapper) + : BaseRepository(authenticationDataContext.Users, mapper) +{ + protected override IIncludableQueryable? Inculdes() + { + return authenticationDataContext.Users + .Include(x => x.Groups)! + .ThenInclude(x => x.Rights); + } +} \ No newline at end of file diff --git a/ApplicationHub.Data.EF/Utils/BaseRepository.cs b/ApplicationHub.Data.EF/Utils/BaseRepository.cs index 3bb0b4e..e5f4733 100644 --- a/ApplicationHub.Data.EF/Utils/BaseRepository.cs +++ b/ApplicationHub.Data.EF/Utils/BaseRepository.cs @@ -2,6 +2,7 @@ using ApplicationHub.Domain.Contracts; using AutoMapper; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; namespace ApplicationHub.Data.EF.Utils; @@ -10,7 +11,15 @@ public class BaseRepository(DbSet dbSet, IMapper mappe public IEnumerable Find(Expression>? where = null, int? limit = null, int? offset = null, List>, OrderDirection>>? order = null) { var resolver = new QueryResolver(); - var result = resolver.Find(dbSet, where, limit, offset, order).ToList(); + var dbSetWithIncludes = Inculdes(); + var result = dbSetWithIncludes == null + ? resolver.Find(dbSet, where, limit, offset, order).ToList() + : resolver.Find(dbSetWithIncludes, where, limit, offset, order).ToList(); return mapper.Map>(result); } + + protected virtual IIncludableQueryable? Inculdes() + { + return null; + } } \ No newline at end of file diff --git a/ApplicationHub.Data.EF/Utils/QueryResolver.cs b/ApplicationHub.Data.EF/Utils/QueryResolver.cs index 9f0ad2e..8549208 100644 --- a/ApplicationHub.Data.EF/Utils/QueryResolver.cs +++ b/ApplicationHub.Data.EF/Utils/QueryResolver.cs @@ -2,11 +2,60 @@ using ApplicationHub.Domain.Contracts; using ApplicationHub.Domain.Contracts.Linq; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; namespace ApplicationHub.Data.EF.Utils; public class QueryResolver where TEntity : class { + public IQueryable Find(IIncludableQueryable dbSet, Expression>? where = null, int? limit = null, int? offset = null, List>, OrderDirection>>? order = null) + { + IQueryable? expression = null; + if (where != null) + { + var whereConverter = new EntityExpressionConverter(where.Parameters.Single()); + expression = dbSet.Where(whereConverter.Convert(where)); + } + + if (offset.HasValue) + { + expression = expression == null + ? dbSet.Skip(offset.Value) + : expression.Skip(offset.Value); + } + + if (limit.HasValue) + { + expression = expression == null + ? dbSet.Take(limit.Value) + : expression.Take(limit.Value); + } + + if (order != null && order.Any()) + { + foreach (var orderDescription in order) + { + var orderConverter = new EntityExpressionConverter(orderDescription.Key.Parameters.Single()); + var convertedOrder = orderConverter.Convert(orderDescription.Key); + switch (orderDescription.Value) + { + case OrderDirection.Asc: + expression = expression == null + ? dbSet.OrderBy(convertedOrder) + : expression.OrderBy(convertedOrder); + break; + case OrderDirection.Desc: + expression = expression == null + ? dbSet.OrderByDescending(convertedOrder) + : expression.OrderByDescending(convertedOrder); + break; + } + } + } + + return expression ?? dbSet.Where(x => true); + } + public IQueryable Find(DbSet dbSet, Expression>? where = null, int? limit = null, int? offset = null, List>, OrderDirection>>? order = null) { IQueryable? expression = null; diff --git a/ApplicationHub/Configuration/AuthenticationConfiguration.cs b/ApplicationHub/Configuration/AuthenticationConfiguration.cs index 6a444d6..ce4f207 100644 --- a/ApplicationHub/Configuration/AuthenticationConfiguration.cs +++ b/ApplicationHub/Configuration/AuthenticationConfiguration.cs @@ -1,4 +1,5 @@ using System.Text; +using ApplicationHub.Jwt.Services; using ApplicationHub.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; @@ -15,6 +16,7 @@ public static class AuthenticationConfiguration public static void Configure(WebApplicationBuilder builder) { var jwtSettings = builder.Configuration.GetSection("JwtSettings"); + builder.Services.AddScoped(); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(x => { x.TokenValidationParameters = new TokenValidationParameters diff --git a/ApplicationHub/Controllers/Authentication/LoginController.cs b/ApplicationHub/Controllers/Authentication/LoginController.cs index cb8fc33..15402a4 100644 --- a/ApplicationHub/Controllers/Authentication/LoginController.cs +++ b/ApplicationHub/Controllers/Authentication/LoginController.cs @@ -1,18 +1,16 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; +using System.Security.Claims; using Adapter.Authentication.Services; +using ApplicationHub.Jwt.Services; using ApplicationHub.Models; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; namespace ApplicationHub.Controllers.Authentication; [Route("/api/login")] [ApiController] -public class LoginController(AuthenticationService authenticationService, IOptions jwtSettings, IPasswordHasher passwordHasher) +public class LoginController(AuthenticationService authenticationService, JwtService jwtService, IOptions jwtSettings, IPasswordHasher passwordHasher) { [HttpPost] public string Login([FromBody] UserLogin login) @@ -27,32 +25,23 @@ public class LoginController(AuthenticationService authenticationService, IOptio throw new ArgumentOutOfRangeException(nameof(login), $"login password not matches or is invalid {foundUser?.PasswordHash ?? ""}"); } - return GenerateJwtToken(login.UserName); + var additionalClaims = new List(); + foreach (var foundUserGroup in foundUser.Groups) + { + additionalClaims.Add(new Claim(foundUserGroup.Name, "true")); + } + + return jwtService.Create( + additionalClaims, + foundUser.UserName, + jwtSettings.Value.Issuer, + jwtSettings.Value.Audience, + jwtSettings.Value.Key, + DateTime.Now.AddMinutes(60)); } catch (Exception) { return ""; } } - - private string GenerateJwtToken(string username) - { - var claims = new[] - { - new Claim(JwtRegisteredClaimNames.Sub, username), - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) - }; - - var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Value.Key)); - var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); - - var token = new JwtSecurityToken( - issuer: jwtSettings.Value.Issuer, - audience: jwtSettings.Value.Audience, - claims: claims, - expires: DateTime.Now.AddMinutes(60), - signingCredentials: creds); - - return new JwtSecurityTokenHandler().WriteToken(token); - } } \ No newline at end of file diff --git a/ApplicationHub/Controllers/Message/GreeterController.cs b/ApplicationHub/Controllers/Message/GreeterController.cs index 469cbef..fef1084 100644 --- a/ApplicationHub/Controllers/Message/GreeterController.cs +++ b/ApplicationHub/Controllers/Message/GreeterController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Authorization; +using ApplicationHub.Jwt; +using ApplicationHub.Jwt.Attributes; using Microsoft.AspNetCore.Mvc; namespace ApplicationHub.Controllers.Message; @@ -13,10 +14,17 @@ public class GreeterController return $"Hello, {name}"; } - [Authorize] + [HasGroup(Group.Admin, Group.User)] [HttpGet("internal")] public string SecureGreet() { - return ""; + return "Hello, Admin"; + } + + [HasGroup(Group.Guest)] + [HttpGet("guest")] + public string SecureGreetGuest() + { + return "Hello, Guest"; } } \ No newline at end of file diff --git a/ApplicationHub/Jwt/Attributes/HasGroup.cs b/ApplicationHub/Jwt/Attributes/HasGroup.cs new file mode 100644 index 0000000..bd1ff67 --- /dev/null +++ b/ApplicationHub/Jwt/Attributes/HasGroup.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace ApplicationHub.Jwt.Attributes; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class HasGroup(params string[] groupNames) : Attribute, IAuthorizationFilter +{ + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!groupNames.Any(groupName => context.HttpContext.User.HasClaim(groupName, "true"))) + { + context.Result = new ForbidResult(); + } + } +} \ No newline at end of file diff --git a/ApplicationHub/Jwt/Group.cs b/ApplicationHub/Jwt/Group.cs new file mode 100644 index 0000000..53ad36f --- /dev/null +++ b/ApplicationHub/Jwt/Group.cs @@ -0,0 +1,8 @@ +namespace ApplicationHub.Jwt; + +public static class Group +{ + public const string Admin = "Group_Name_Administrator"; + public const string User = "Group_Name_User"; + public const string Guest = "Group_Name_Guest"; +} \ No newline at end of file diff --git a/ApplicationHub/Jwt/Services/JwtService.cs b/ApplicationHub/Jwt/Services/JwtService.cs new file mode 100644 index 0000000..90dfd0e --- /dev/null +++ b/ApplicationHub/Jwt/Services/JwtService.cs @@ -0,0 +1,32 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace ApplicationHub.Jwt.Services; + +public class JwtService +{ + public string Create(List additionalClaims, string subscriber, string issuer, string audience, string signingKey, DateTime expires) + { + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, subscriber), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + claims.AddRange(additionalClaims); + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512); + + var token = new JwtSecurityToken( + issuer: issuer, + audience: audience, + claims: claims, + expires: expires, + signingCredentials: creds); + + return new JwtSecurityTokenHandler().WriteToken(token); + } +} \ No newline at end of file diff --git a/ApplicationHub/appsettings.Development.json b/ApplicationHub/appsettings.Development.json index 4a52192..f0ca935 100644 --- a/ApplicationHub/appsettings.Development.json +++ b/ApplicationHub/appsettings.Development.json @@ -11,6 +11,6 @@ "JwtSettings": { "Issuer": "https://deine-api.com", "Audience": "https://deine-app.com", - "Key": "DeinLangerGeheimerSchluesselFuerJWT" + "Key": "DeinLangerGeheimerSchluesselFuerJWTEinsZweiDreiVierFuenfSechs>.<" } } diff --git a/ApplicationHub/appsettings.json b/ApplicationHub/appsettings.json index aae2109..504be54 100644 --- a/ApplicationHub/appsettings.json +++ b/ApplicationHub/appsettings.json @@ -12,6 +12,6 @@ "JwtSettings": { "Issuer": "https://deine-api.com", "Audience": "https://deine-app.com", - "Key": "DeinLangerGeheimerSchluesselFuerJWT" + "Key": "DeinLangerGeheimerSchluesselFuerJWTEinsZweiDreiVierFuenfSechs>.<" } }