better JWT creation and add Group based Authentication

This commit is contained in:
admin 2025-08-09 22:02:00 +02:00
parent a784630322
commit 2c457db5ae
12 changed files with 168 additions and 35 deletions

View File

@ -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<Group, GroupEntity>(authenticationDataContext.Groups, mapper);
public class GroupRepository(AuthenticationDataContext authenticationDataContext, IMapper mapper) : BaseRepository<Group, GroupEntity>(authenticationDataContext.Groups, mapper)
{
protected override IIncludableQueryable<GroupEntity, object?>? Inculdes()
{
return authenticationDataContext.Groups
.Include(x => x.Rights);
}
}

View File

@ -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<User, UserEntity>(authenticationDataContext.Users, mapper);
public class UserRepository(AuthenticationDataContext authenticationDataContext, IMapper mapper)
: BaseRepository<User, UserEntity>(authenticationDataContext.Users, mapper)
{
protected override IIncludableQueryable<UserEntity, object?>? Inculdes()
{
return authenticationDataContext.Users
.Include(x => x.Groups)!
.ThenInclude(x => x.Rights);
}
}

View File

@ -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<TModel, TEntity>(DbSet<TEntity> dbSet, IMapper mappe
public IEnumerable<TModel> Find(Expression<Func<TModel, bool>>? where = null, int? limit = null, int? offset = null, List<KeyValuePair<Expression<Func<TModel, bool>>, OrderDirection>>? order = null)
{
var resolver = new QueryResolver<TModel, TEntity>();
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<List<TModel>>(result);
}
protected virtual IIncludableQueryable<TEntity, object?>? Inculdes()
{
return null;
}
}

View File

@ -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<TModel, TEntity> where TEntity : class
{
public IQueryable<TEntity> Find(IIncludableQueryable<TEntity, object?> dbSet, Expression<Func<TModel, bool>>? where = null, int? limit = null, int? offset = null, List<KeyValuePair<Expression<Func<TModel, bool>>, OrderDirection>>? order = null)
{
IQueryable<TEntity>? expression = null;
if (where != null)
{
var whereConverter = new EntityExpressionConverter<TModel, TEntity>(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<TModel, TEntity>(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<TEntity> Find(DbSet<TEntity> dbSet, Expression<Func<TModel, bool>>? where = null, int? limit = null, int? offset = null, List<KeyValuePair<Expression<Func<TModel, bool>>, OrderDirection>>? order = null)
{
IQueryable<TEntity>? expression = null;

View File

@ -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<JwtService>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(x =>
{
x.TokenValidationParameters = new TokenValidationParameters

View File

@ -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> jwtSettings, IPasswordHasher<UserLogin> passwordHasher)
public class LoginController(AuthenticationService authenticationService, JwtService jwtService, IOptions<JwtSettings> jwtSettings, IPasswordHasher<UserLogin> 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 ?? "<null>"}");
}
return GenerateJwtToken(login.UserName);
var additionalClaims = new List<Claim>();
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);
}
}

View File

@ -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";
}
}

View File

@ -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();
}
}
}

View File

@ -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";
}

View File

@ -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<Claim> additionalClaims, string subscriber, string issuer, string audience, string signingKey, DateTime expires)
{
var claims = new List<Claim>
{
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);
}
}

View File

@ -11,6 +11,6 @@
"JwtSettings": {
"Issuer": "https://deine-api.com",
"Audience": "https://deine-app.com",
"Key": "DeinLangerGeheimerSchluesselFuerJWT"
"Key": "DeinLangerGeheimerSchluesselFuerJWTEinsZweiDreiVierFuenfSechs>.<"
}
}

View File

@ -12,6 +12,6 @@
"JwtSettings": {
"Issuer": "https://deine-api.com",
"Audience": "https://deine-app.com",
"Key": "DeinLangerGeheimerSchluesselFuerJWT"
"Key": "DeinLangerGeheimerSchluesselFuerJWTEinsZweiDreiVierFuenfSechs>.<"
}
}