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.Data.EF.Utils;
|
||||||
using ApplicationHub.Domain.Contracts.Authentication.Models;
|
using ApplicationHub.Domain.Contracts.Authentication.Models;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
|
|
||||||
namespace ApplicationHub.Data.EF.Authentication.Repositories;
|
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.Data.EF.Utils;
|
||||||
using ApplicationHub.Domain.Contracts.Authentication.Models;
|
using ApplicationHub.Domain.Contracts.Authentication.Models;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
|
|
||||||
namespace ApplicationHub.Data.EF.Authentication.Repositories;
|
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 ApplicationHub.Domain.Contracts;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
|
|
||||||
namespace ApplicationHub.Data.EF.Utils;
|
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)
|
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 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);
|
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;
|
||||||
using ApplicationHub.Domain.Contracts.Linq;
|
using ApplicationHub.Domain.Contracts.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query;
|
||||||
|
|
||||||
namespace ApplicationHub.Data.EF.Utils;
|
namespace ApplicationHub.Data.EF.Utils;
|
||||||
|
|
||||||
public class QueryResolver<TModel, TEntity> where TEntity : class
|
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)
|
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;
|
IQueryable<TEntity>? expression = null;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using ApplicationHub.Jwt.Services;
|
||||||
using ApplicationHub.Models;
|
using ApplicationHub.Models;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
@ -15,6 +16,7 @@ public static class AuthenticationConfiguration
|
|||||||
public static void Configure(WebApplicationBuilder builder)
|
public static void Configure(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
|
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
|
||||||
|
builder.Services.AddScoped<JwtService>();
|
||||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(x =>
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(x =>
|
||||||
{
|
{
|
||||||
x.TokenValidationParameters = new TokenValidationParameters
|
x.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
|||||||
@ -1,18 +1,16 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.Security.Claims;
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Text;
|
|
||||||
using Adapter.Authentication.Services;
|
using Adapter.Authentication.Services;
|
||||||
|
using ApplicationHub.Jwt.Services;
|
||||||
using ApplicationHub.Models;
|
using ApplicationHub.Models;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
|
|
||||||
namespace ApplicationHub.Controllers.Authentication;
|
namespace ApplicationHub.Controllers.Authentication;
|
||||||
|
|
||||||
[Route("/api/login")]
|
[Route("/api/login")]
|
||||||
[ApiController]
|
[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]
|
[HttpPost]
|
||||||
public string Login([FromBody] UserLogin login)
|
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>"}");
|
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)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
return "";
|
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;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace ApplicationHub.Controllers.Message;
|
namespace ApplicationHub.Controllers.Message;
|
||||||
@ -13,10 +14,17 @@ public class GreeterController
|
|||||||
return $"Hello, {name}";
|
return $"Hello, {name}";
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize]
|
[HasGroup(Group.Admin, Group.User)]
|
||||||
[HttpGet("internal")]
|
[HttpGet("internal")]
|
||||||
public string SecureGreet()
|
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": {
|
"JwtSettings": {
|
||||||
"Issuer": "https://deine-api.com",
|
"Issuer": "https://deine-api.com",
|
||||||
"Audience": "https://deine-app.com",
|
"Audience": "https://deine-app.com",
|
||||||
"Key": "DeinLangerGeheimerSchluesselFuerJWT"
|
"Key": "DeinLangerGeheimerSchluesselFuerJWTEinsZweiDreiVierFuenfSechs>.<"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,6 @@
|
|||||||
"JwtSettings": {
|
"JwtSettings": {
|
||||||
"Issuer": "https://deine-api.com",
|
"Issuer": "https://deine-api.com",
|
||||||
"Audience": "https://deine-app.com",
|
"Audience": "https://deine-app.com",
|
||||||
"Key": "DeinLangerGeheimerSchluesselFuerJWT"
|
"Key": "DeinLangerGeheimerSchluesselFuerJWTEinsZweiDreiVierFuenfSechs>.<"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user