better JWT creation and add Group based Authentication
This commit is contained in:
parent
a784630322
commit
2c457db5ae
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
16
ApplicationHub/Jwt/Attributes/HasGroup.cs
Normal file
16
ApplicationHub/Jwt/Attributes/HasGroup.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
8
ApplicationHub/Jwt/Group.cs
Normal file
8
ApplicationHub/Jwt/Group.cs
Normal 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";
|
||||
}
|
||||
32
ApplicationHub/Jwt/Services/JwtService.cs
Normal file
32
ApplicationHub/Jwt/Services/JwtService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,6 @@
|
||||
"JwtSettings": {
|
||||
"Issuer": "https://deine-api.com",
|
||||
"Audience": "https://deine-app.com",
|
||||
"Key": "DeinLangerGeheimerSchluesselFuerJWT"
|
||||
"Key": "DeinLangerGeheimerSchluesselFuerJWTEinsZweiDreiVierFuenfSechs>.<"
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,6 @@
|
||||
"JwtSettings": {
|
||||
"Issuer": "https://deine-api.com",
|
||||
"Audience": "https://deine-app.com",
|
||||
"Key": "DeinLangerGeheimerSchluesselFuerJWT"
|
||||
"Key": "DeinLangerGeheimerSchluesselFuerJWTEinsZweiDreiVierFuenfSechs>.<"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user