Switch to JWT authentication (#19)
* switch to jwt authentication * fix delete files * adjust saving of deletion of all files * update api to main/jwt Co-authored-by: rootdarkarchon <root.darkarchon@outlook.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
using MareSynchronos.API;
|
||||
using MareSynchronosShared;
|
||||
using MareSynchronosShared.Authentication;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronosServer.Controllers;
|
||||
|
||||
[AllowAnonymous]
|
||||
[Route(MareAuth.Auth)]
|
||||
public class JwtController : Controller
|
||||
{
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
private readonly SecretKeyAuthenticatorService _secretKeyAuthenticatorService;
|
||||
private readonly IConfigurationService<MareConfigurationAuthBase> _configuration;
|
||||
|
||||
public JwtController(IHttpContextAccessor accessor, SecretKeyAuthenticatorService secretKeyAuthenticatorService, IConfigurationService<MareConfigurationAuthBase> configuration)
|
||||
{
|
||||
_accessor = accessor;
|
||||
_secretKeyAuthenticatorService = secretKeyAuthenticatorService;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost(MareAuth.AuthCreate)]
|
||||
public async Task<IActionResult> CreateToken(string auth)
|
||||
{
|
||||
if (string.IsNullOrEmpty(auth)) return BadRequest("No Authkey");
|
||||
|
||||
var ip = _accessor.GetIpAddress();
|
||||
|
||||
var authResult = await _secretKeyAuthenticatorService.AuthorizeAsync(ip, auth);
|
||||
|
||||
if (!authResult.Success) return Unauthorized("Invalid Authkey");
|
||||
|
||||
var token = CreateToken(new List<Claim>()
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, authResult.Uid)
|
||||
});
|
||||
|
||||
return Content(token.RawData);
|
||||
}
|
||||
|
||||
private JwtSecurityToken CreateToken(IEnumerable<Claim> authClaims)
|
||||
{
|
||||
var authSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration.GetValue<string>(nameof(MareConfigurationAuthBase.Jwt))));
|
||||
|
||||
var token = new SecurityTokenDescriptor()
|
||||
{
|
||||
Subject = new ClaimsIdentity(authClaims),
|
||||
SigningCredentials = new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256Signature)
|
||||
};
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
return handler.CreateJwtSecurityToken(token);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
using Google.Protobuf;
|
||||
using Grpc.Core;
|
||||
@@ -9,6 +8,7 @@ using MareSynchronosShared.Models;
|
||||
using MareSynchronosShared.Protos;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
@@ -37,7 +37,7 @@ public partial class MareHub
|
||||
request.Hash.AddRange(ownFiles.Select(f => f.Hash));
|
||||
Metadata headers = new Metadata()
|
||||
{
|
||||
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value }
|
||||
{ "Authorization", Context.GetHttpContext().Request.Headers["Authorization"].ToString() }
|
||||
};
|
||||
_ = await _fileServiceClient.DeleteFilesAsync(request, headers).ConfigureAwait(false);
|
||||
}
|
||||
@@ -213,7 +213,7 @@ public partial class MareHub
|
||||
|
||||
Metadata headers = new Metadata()
|
||||
{
|
||||
{ "Authorization", Context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.Authentication, StringComparison.Ordinal))?.Value }
|
||||
{ "Authorization", Context.GetHttpContext().Request.Headers["Authorization"].ToString() }
|
||||
};
|
||||
var streamingCall = _fileServiceClient.UploadFile(headers);
|
||||
using var tempFileStream = new FileStream(tempFileName, FileMode.Open, FileAccess.Read);
|
||||
|
||||
@@ -14,6 +14,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public partial class MareHub : Hub<IMareHub>, IMareHub
|
||||
{
|
||||
private readonly MareMetrics _mareMetrics;
|
||||
@@ -118,6 +119,7 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
|
||||
return Task.FromResult(needsReconnect);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
_logger.LogCallInfo(MareHubLogger.Args(_contextAccessor.GetIpAddress()));
|
||||
@@ -125,6 +127,7 @@ public partial class MareHub : Hub<IMareHub>, IMareHub
|
||||
await base.OnConnectedAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Authorize(Policy = "Authenticated")]
|
||||
public override async Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
_mareMetrics.DecGauge(MetricsAPI.GaugeConnections);
|
||||
|
||||
@@ -31,11 +31,14 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.25.1" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="7.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.25.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using MareSynchronos.API;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MareSynchronosServer.Hubs;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -20,6 +19,9 @@ using MareSynchronosShared.Services;
|
||||
using Prometheus;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Grpc.Net.ClientFactory;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronosServer;
|
||||
|
||||
@@ -62,6 +64,7 @@ public class Startup
|
||||
ConfigureMareServices(services, mareConfig);
|
||||
|
||||
services.AddHealthChecks();
|
||||
services.AddControllers();
|
||||
}
|
||||
|
||||
private static void ConfigureMareServices(IServiceCollection services, IConfigurationSection mareConfig)
|
||||
@@ -121,17 +124,35 @@ public class Startup
|
||||
{
|
||||
services.AddSingleton<SecretKeyAuthenticatorService>();
|
||||
services.AddTransient<IAuthorizationHandler, UserRequirementHandler>();
|
||||
services.AddAuthentication(SecretKeyAuthenticationHandler.AuthScheme)
|
||||
.AddScheme<AuthenticationSchemeOptions, SecretKeyAuthenticationHandler>(SecretKeyAuthenticationHandler.AuthScheme, options => { options.Validate(); });
|
||||
|
||||
services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
|
||||
.Configure<IConfigurationService<MareConfigurationAuthBase>>((o, s) =>
|
||||
{
|
||||
o.TokenValidationParameters = new()
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateLifetime = false,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(s.GetValue<string>(nameof(MareConfigurationAuthBase.Jwt))))
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthentication(o =>
|
||||
{
|
||||
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer();
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.DefaultPolicy = new AuthorizationPolicyBuilder()
|
||||
.AddAuthenticationSchemes(SecretKeyAuthenticationHandler.AuthScheme)
|
||||
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
|
||||
.RequireAuthenticatedUser().Build();
|
||||
options.AddPolicy("Authenticated", policy =>
|
||||
{
|
||||
policy.AddAuthenticationSchemes(SecretKeyAuthenticationHandler.AuthScheme);
|
||||
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
|
||||
policy.RequireAuthenticatedUser();
|
||||
});
|
||||
options.AddPolicy("Identified", policy =>
|
||||
@@ -306,7 +327,8 @@ public class Startup
|
||||
endpoints.MapGrpcService<GrpcConfigurationService<ServerConfiguration>>().AllowAnonymous();
|
||||
}
|
||||
|
||||
endpoints.MapHealthChecks("/health").WithMetadata(new AllowAnonymousAttribute());
|
||||
endpoints.MapHealthChecks("/health").AllowAnonymous();
|
||||
endpoints.MapControllers().AllowAnonymous();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace MareSynchronosShared.Authentication;
|
||||
|
||||
internal record SecretKeyAuthReply(bool Success, string? Uid);
|
||||
public record SecretKeyAuthReply(bool Success, string? Uid);
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosShared.Authentication;
|
||||
|
||||
public class SecretKeyAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
public const string AuthScheme = "SecretKeyGrpcAuth";
|
||||
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
private readonly SecretKeyAuthenticatorService secretKeyAuthenticatorService;
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> IPLocks = new(StringComparer.Ordinal);
|
||||
|
||||
public SecretKeyAuthenticationHandler(IHttpContextAccessor accessor, SecretKeyAuthenticatorService secretKeyAuthenticatorService,
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
|
||||
{
|
||||
_accessor = accessor;
|
||||
this.secretKeyAuthenticatorService = secretKeyAuthenticatorService;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var endpoint = Context.GetEndpoint();
|
||||
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
|
||||
{
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
|
||||
if (!Request.Headers.TryGetValue("Authorization", out var authHeader))
|
||||
{
|
||||
return AuthenticateResult.Fail("Failed Authorization");
|
||||
}
|
||||
|
||||
var ip = _accessor.GetIpAddress();
|
||||
|
||||
if (!IPLocks.TryGetValue(ip, out var semaphore))
|
||||
{
|
||||
semaphore = new SemaphoreSlim(1);
|
||||
IPLocks[ip] = semaphore;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await semaphore.WaitAsync(Context.RequestAborted).ConfigureAwait(false);
|
||||
var authResult = await secretKeyAuthenticatorService.AuthorizeAsync(ip, authHeader).ConfigureAwait(false);
|
||||
|
||||
if (!authResult.Success)
|
||||
{
|
||||
return AuthenticateResult.Fail("Failed Authorization");
|
||||
}
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, authResult.Uid),
|
||||
new(ClaimTypes.Authentication, authHeader)
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, nameof(SecretKeyAuthenticationHandler));
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
|
||||
return AuthenticateResult.Success(ticket);
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using MareSynchronosShared.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosShared.Authentication;
|
||||
|
||||
@@ -27,11 +26,11 @@ public class SecretKeyAuthenticatorService
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
internal async Task<SecretKeyAuthReply> AuthorizeAsync(string ip, string secretKey)
|
||||
public async Task<SecretKeyAuthReply> AuthorizeAsync(string ip, string hashedSecretKey)
|
||||
{
|
||||
_metrics.IncCounter(MetricsAPI.CounterAuthenticationRequests);
|
||||
|
||||
if (_cachedPositiveResponses.TryGetValue(secretKey, out var cachedPositiveResponse))
|
||||
if (_cachedPositiveResponses.TryGetValue(hashedSecretKey, out var cachedPositiveResponse))
|
||||
{
|
||||
_metrics.IncCounter(MetricsAPI.CounterAuthenticationCacheHits);
|
||||
return cachedPositiveResponse;
|
||||
@@ -58,8 +57,7 @@ public class SecretKeyAuthenticatorService
|
||||
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
using var context = scope.ServiceProvider.GetService<MareDbContext>();
|
||||
var hashedHeader = StringUtils.Sha256String(secretKey);
|
||||
var authReply = await context.Auth.AsNoTracking().SingleOrDefaultAsync(u => u.HashedKey == hashedHeader).ConfigureAwait(false);
|
||||
var authReply = await context.Auth.AsNoTracking().SingleOrDefaultAsync(u => u.HashedKey == hashedSecretKey).ConfigureAwait(false);
|
||||
|
||||
SecretKeyAuthReply reply = new(authReply != null, authReply?.UserUID);
|
||||
|
||||
@@ -67,11 +65,11 @@ public class SecretKeyAuthenticatorService
|
||||
{
|
||||
_metrics.IncCounter(MetricsAPI.CounterAuthenticationSuccesses);
|
||||
|
||||
_cachedPositiveResponses[secretKey] = reply;
|
||||
_cachedPositiveResponses[hashedSecretKey] = reply;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(5)).ConfigureAwait(false);
|
||||
_cachedPositiveResponses.TryRemove(secretKey, out _);
|
||||
_cachedPositiveResponses.TryRemove(hashedSecretKey, out _);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ public class MareConfigurationAuthBase : MareConfigurationBase
|
||||
public int TempBanDurationInMinutes { get; set; } = 5;
|
||||
[RemoteConfiguration]
|
||||
public List<string> WhitelistedIps { get; set; } = new();
|
||||
[RemoteConfiguration]
|
||||
public string Jwt { get; set; } = string.Empty;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
@@ -19,6 +21,7 @@ public class MareConfigurationAuthBase : MareConfigurationBase
|
||||
sb.AppendLine($"{nameof(MainServerGrpcAddress)} => {MainServerGrpcAddress}");
|
||||
sb.AppendLine($"{nameof(FailedAuthForTempBan)} => {FailedAuthForTempBan}");
|
||||
sb.AppendLine($"{nameof(TempBanDurationInMinutes)} => {TempBanDurationInMinutes}");
|
||||
sb.AppendLine($"{nameof(Jwt)} => {Jwt}");
|
||||
sb.AppendLine($"{nameof(WhitelistedIps)} => {string.Join(", ", WhitelistedIps)}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -78,14 +78,16 @@ public class GrpcFileService : FileService.FileServiceBase
|
||||
try
|
||||
{
|
||||
var fi = FilePathUtil.GetFileInfoForHash(_basePath, hash);
|
||||
fi?.Delete();
|
||||
var file = await _mareDbContext.Files.SingleOrDefaultAsync(f => f.Hash == hash).ConfigureAwait(false);
|
||||
if (file != null)
|
||||
if (file != null && fi != null)
|
||||
{
|
||||
_mareDbContext.Files.Remove(file);
|
||||
await _mareDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_metricsClient.DecGauge(MetricsAPI.GaugeFilesTotal, fi == null ? 0 : 1);
|
||||
_metricsClient.DecGauge(MetricsAPI.GaugeFilesTotalSize, fi?.Length ?? 0);
|
||||
|
||||
fi?.Delete();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -94,7 +96,6 @@ public class GrpcFileService : FileService.FileServiceBase
|
||||
}
|
||||
}
|
||||
|
||||
await _mareDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
return new Empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="7.0.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -7,10 +7,13 @@ using MareSynchronosShared.Protos;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Prometheus;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronosStaticFilesServer;
|
||||
|
||||
@@ -101,11 +104,26 @@ public class Startup
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthentication(options =>
|
||||
services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
|
||||
.Configure<IConfigurationService<MareConfigurationAuthBase>>((o, s) =>
|
||||
{
|
||||
options.DefaultScheme = SecretKeyAuthenticationHandler.AuthScheme;
|
||||
})
|
||||
.AddScheme<AuthenticationSchemeOptions, SecretKeyAuthenticationHandler>(SecretKeyAuthenticationHandler.AuthScheme, options => { });
|
||||
o.TokenValidationParameters = new()
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateLifetime = false,
|
||||
ValidateAudience = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(s.GetValue<string>(nameof(MareConfigurationAuthBase.Jwt))))
|
||||
};
|
||||
});
|
||||
|
||||
services.AddAuthentication(o =>
|
||||
{
|
||||
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer();
|
||||
|
||||
services.AddAuthorization(options => options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
|
||||
|
||||
if (_isMain)
|
||||
|
||||
Reference in New Issue
Block a user