Initial commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
namespace Backend.Security.Authentication {
|
||||
public static class JwtTokenAuthentication {
|
||||
public const string Scheme = "JwtTokenAuthentication";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Backend.Options;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace Backend.Security.Authentication {
|
||||
public static class JwtTokenAuthenticationExtensions {
|
||||
public static AuthenticationBuilder AddJwtTokenAuthentication(this AuthenticationBuilder builder,
|
||||
IConfiguration configuration) {
|
||||
builder.Services.AddOptionsFromConfiguration<JwtTokenAuthenticationOptions>(configuration);
|
||||
|
||||
return builder.AddScheme<JwtTokenAuthenticationHandlerOptions, JwtTokenAuthenticationHandler>(
|
||||
JwtTokenAuthentication.Scheme,
|
||||
_ => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Security.Claims;
|
||||
using Backend.Entitys;
|
||||
using Backend.Repositorys;
|
||||
using Backend.Security.Authorization;
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
namespace Backend.Security.Authentication {
|
||||
public class JwtTokenAuthenticationHandler : AuthenticationHandler<JwtTokenAuthenticationHandlerOptions> {
|
||||
private readonly TokenRepository _tokens;
|
||||
private readonly GroupRepository _groups;
|
||||
private readonly JwtTokenAuthenticationOptions _options;
|
||||
|
||||
public JwtTokenAuthenticationHandler(
|
||||
IOptionsMonitor<JwtTokenAuthenticationHandlerOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock,
|
||||
IOptions<JwtTokenAuthenticationOptions> tokenOptions,
|
||||
TokenRepository tokens,
|
||||
GroupRepository groups)
|
||||
: base(options, logger, encoder, clock) {
|
||||
_options = tokenOptions.Value;
|
||||
_tokens = tokens;
|
||||
_groups = groups;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
|
||||
if (Request.Headers["Authorization"].Equals(_options.DebugAccessToken))
|
||||
return AuthenticateResult.Success(GetAuthenticationTicket(null, null, "*"));
|
||||
|
||||
var accessToken = GetAccessToken();
|
||||
if (accessToken == null) return AuthenticateResult.Fail("Access Token invalid");
|
||||
var refreshToken = _tokens.GetRefreshToken(accessToken.RefreshTokenId);
|
||||
if (refreshToken == null) return AuthenticateResult.Fail("Refresh Token invalid");
|
||||
if (!_tokens.ValidateRefreshToken(refreshToken.Id)) return AuthenticateResult.Fail("Refresh Token invalid");
|
||||
bool valid = _tokens.ValidateAccessToken(accessToken.Id);
|
||||
return valid
|
||||
? AuthenticateResult.Success(GetAuthenticationTicket(accessToken, refreshToken))
|
||||
: AuthenticateResult.Fail("Access Token invalid");
|
||||
}
|
||||
|
||||
private AuthenticationTicket GetAuthenticationTicket(AccessToken accessToken, RefreshToken refreshToken, params string[] customPerms) {
|
||||
List<Claim> claims = GenerateClaims(accessToken, refreshToken, customPerms);
|
||||
ClaimsPrincipal principal = new ClaimsPrincipal();
|
||||
principal.AddIdentity(new ClaimsIdentity(claims, JwtTokenAuthentication.Scheme));
|
||||
AuthenticationTicket ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
return ticket;
|
||||
}
|
||||
|
||||
private List<Claim> GenerateClaims(AccessToken accessToken, RefreshToken refreshToken, params string[] customPerms) {
|
||||
List<Claim> claims = new List<Claim>();
|
||||
if (accessToken is not null && refreshToken is not null) {
|
||||
claims.AddRange(new List<Claim> {
|
||||
new(CustomClaimTypes.AccessTokenId, accessToken.Id.ToString()),
|
||||
new(CustomClaimTypes.RefreshTokenId, refreshToken.Id.ToString()),
|
||||
new(CustomClaimTypes.UserId, refreshToken.UserId.ToString()),
|
||||
});
|
||||
|
||||
string[] permissions = _groups.GetUserPermissions(refreshToken.UserId).Select(perm => perm.PermissionKey).ToArray();
|
||||
claims.AddRange(permissions
|
||||
.Select(permission => new Claim(CustomClaimTypes.Permission, permission)));
|
||||
}
|
||||
|
||||
claims.AddRange(customPerms.Select(perm => new Claim(CustomClaimTypes.Permission, perm)));
|
||||
|
||||
return claims;
|
||||
}
|
||||
|
||||
private AccessToken GetAccessToken() {
|
||||
string key = Request.Headers["Authorization"];
|
||||
if (string.IsNullOrEmpty(key)) {
|
||||
key = Request.Query["token"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return null;
|
||||
|
||||
AccessToken token = _tokens.GetAccessToken(Guid.Parse(key));
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
@@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace Backend.Security.Authentication {
|
||||
public class JwtTokenAuthenticationHandlerOptions : AuthenticationSchemeOptions {
|
||||
// Options for the authentication handler.
|
||||
// Currently: None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Backend.Options;
|
||||
|
||||
namespace Backend.Security.Authentication {
|
||||
public class JwtTokenAuthenticationOptions : OptionsFromConfiguration {
|
||||
public override string Position => "Authentication";
|
||||
|
||||
public string RefreshTokenExpirationTimeInHours { get; set; }
|
||||
public string AccessTokenExpirationTimeInMinutes { get; set; }
|
||||
public string DebugAccessToken { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Backend.Options {
|
||||
public abstract class OptionsFromConfiguration {
|
||||
public abstract string Position { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Backend.Options {
|
||||
public static class OptionsFromConfigurationExtensions {
|
||||
public static T AddOptionsFromConfiguration<T>(this IServiceCollection services, IConfiguration configuration)
|
||||
where T : OptionsFromConfiguration {
|
||||
T optionsInstance = (T)Activator.CreateInstance(typeof(T));
|
||||
if (optionsInstance == null) return null;
|
||||
string position = optionsInstance.Position;
|
||||
services.Configure((Action<T>)(options => {
|
||||
IConfigurationSection section = configuration.GetSection(position);
|
||||
if (section != null) {
|
||||
section.Bind(options);
|
||||
}
|
||||
}));
|
||||
return optionsInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Backend.Security.Authorization {
|
||||
public sealed class AuthorizedAttribute : TypeFilterAttribute {
|
||||
public AuthorizedAttribute(params string[] permission) : base(typeof(AuthorizedFilter)) {
|
||||
Arguments = new object[] { permission };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Backend.Security.Authorization {
|
||||
public class AuthorizedFilter : IAuthorizationFilter {
|
||||
private readonly string[] _permissions;
|
||||
|
||||
public AuthorizedFilter(params string[] permissions) {
|
||||
_permissions = permissions;
|
||||
}
|
||||
|
||||
public void OnAuthorization(AuthorizationFilterContext context) {
|
||||
if (EndpointHasAllowAnonymousFilter(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsAuthenticated(context)) {
|
||||
context.Result = new UnauthorizedResult();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ContainsRequiredRole(context)) {
|
||||
context.Result = new ForbidResult();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EndpointHasAllowAnonymousFilter(AuthorizationFilterContext context) {
|
||||
return context.Filters.Any(item => item is IAllowAnonymousFilter);
|
||||
}
|
||||
|
||||
private bool IsAuthenticated(AuthorizationFilterContext context) {
|
||||
return context.HttpContext.User.Identity.IsAuthenticated;
|
||||
}
|
||||
|
||||
private bool ContainsRequiredRole(AuthorizationFilterContext context) {
|
||||
if (context.HttpContext.User.HasClaim(CustomClaimTypes.Permission, "*"))
|
||||
return true;
|
||||
|
||||
var perms = context.HttpContext.User.Claims
|
||||
.Where(c => c.Type == CustomClaimTypes.Permission)
|
||||
.Select(c => c.Value).ToArray();
|
||||
|
||||
if (context.RouteData.Values.ContainsKey("userId")) {
|
||||
var accessedUser = context.RouteData.Values["userId"] as string;
|
||||
|
||||
if (accessedUser == context.HttpContext.User.GetUserId()) {
|
||||
var selfPerms = _permissions.Where(p => p.StartsWith("self.")).ToArray();
|
||||
|
||||
if (!selfPerms.Any())
|
||||
return true;
|
||||
|
||||
if (CheckPermission(selfPerms, perms))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CheckPermission(_permissions, perms.Where(p => !p.StartsWith("self.")).ToArray()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
|
||||
bool CheckPermission(string[] permissions, string[] permission) {
|
||||
if (permissions.Length == 0)
|
||||
return true;
|
||||
|
||||
if (permission.Contains("*"))
|
||||
return true;
|
||||
|
||||
foreach (var perm in permissions) {
|
||||
if (permission.Contains(perm))
|
||||
return true;
|
||||
|
||||
string[] splice = perm.Split(".");
|
||||
string cache = "";
|
||||
foreach (var s in splice) {
|
||||
cache += s + ".";
|
||||
if (permission.Contains(cache + "*"))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Backend.Security.Authorization {
|
||||
public static class ClaimsPrincipalExtensions {
|
||||
public static string GetAccessTokenId(this ClaimsPrincipal principal) =>
|
||||
principal.FindFirstValue(CustomClaimTypes.AccessTokenId);
|
||||
|
||||
public static string GetRefreshTokenId(this ClaimsPrincipal principal) =>
|
||||
principal.FindFirstValue(CustomClaimTypes.RefreshTokenId);
|
||||
|
||||
public static string GetUserId(this ClaimsPrincipal principal) =>
|
||||
principal.FindFirstValue(CustomClaimTypes.UserId);
|
||||
|
||||
public static string[] GetPermissions(this ClaimsPrincipal principal) => principal.Claims
|
||||
.Where(claim => claim.Type.Equals(CustomClaimTypes.Permission))
|
||||
.Select(claim => claim.Value)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Backend.Security.Authorization {
|
||||
public static class CustomClaimTypes {
|
||||
public const string AccessTokenId = "WebDesktop.AccessTokenId";
|
||||
public const string RefreshTokenId = "WebDesktop.RefreshTokenId";
|
||||
public const string UserId = "WebDesktop.UserId";
|
||||
public const string Permission = "WebDesktop.Permission";
|
||||
}
|
||||
}
|
||||
9
Backend/Security/ITokenContext.cs
Normal file
9
Backend/Security/ITokenContext.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Backend.Security {
|
||||
public interface ITokenContext {
|
||||
bool IsAuthenticated { get; }
|
||||
Guid UserId { get; }
|
||||
Guid AccessTokenId { get; }
|
||||
Guid RefreshTokenId { get; }
|
||||
string[] Permissions { get; }
|
||||
}
|
||||
}
|
||||
14
Backend/Security/Permissions.cs
Normal file
14
Backend/Security/Permissions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Backend.Security;
|
||||
|
||||
public static class Permissions {
|
||||
|
||||
public const string ShowUsers = "users.see";
|
||||
public const string EditUsers = "users.edit";
|
||||
public const string DeleteUsers = "users.delete";
|
||||
public const string LogoutUsers = "users.logout";
|
||||
public const string EditUserPermissions = "users.permissions.edit";
|
||||
public const string ShowUserPermissions = "users.permissions.show";
|
||||
|
||||
public const string EditOwnPermissions = "self.permissions.edit";
|
||||
|
||||
}
|
||||
26
Backend/Security/TokenContext.cs
Normal file
26
Backend/Security/TokenContext.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Backend.Security.Authorization;
|
||||
|
||||
namespace Backend.Security {
|
||||
internal class TokenContext : ITokenContext {
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
|
||||
public TokenContext(IHttpContextAccessor accessor) {
|
||||
_accessor = accessor;
|
||||
}
|
||||
|
||||
public bool IsAuthenticated => _accessor.HttpContext?.User.Identity?.IsAuthenticated == true;
|
||||
|
||||
public Guid UserId => CreateGuild(_accessor.HttpContext?.User.GetUserId());
|
||||
|
||||
public Guid AccessTokenId => CreateGuild(_accessor.HttpContext?.User.GetAccessTokenId());
|
||||
|
||||
public Guid RefreshTokenId => CreateGuild(_accessor.HttpContext?.User.GetRefreshTokenId());
|
||||
|
||||
public string[] Permissions => _accessor.HttpContext?.User.GetPermissions();
|
||||
|
||||
private static Guid CreateGuild(string id) {
|
||||
if (string.IsNullOrEmpty(id)) return Guid.Empty;
|
||||
return Guid.Parse(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user