diff --git a/Docker/run/config/sharded/server-shard-main.json b/Docker/run/config/sharded/server-shard-main.json index f21b471..782fa8c 100644 --- a/Docker/run/config/sharded/server-shard-main.json +++ b/Docker/run/config/sharded/server-shard-main.json @@ -31,6 +31,7 @@ "MainServerGrpcAddress": "", "FailedAuthForTempBan": 5, "TempBanDurationInMinutes": 5, + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring", "WhitelistedIps": [ "" ], diff --git a/Docker/run/config/standalone/server-standalone.json b/Docker/run/config/standalone/server-standalone.json index 1d593b0..d154f8f 100644 --- a/Docker/run/config/standalone/server-standalone.json +++ b/Docker/run/config/standalone/server-standalone.json @@ -31,6 +31,7 @@ "MainServerGrpcAddress": "", "FailedAuthForTempBan": 5, "TempBanDurationInMinutes": 5, + "Jwt": "teststringteststringteststringteststringteststringteststringteststringteststringteststringteststring", "WhitelistedIps": [ "" ], diff --git a/MareAPI b/MareAPI index c01c499..6645eaf 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit c01c4990d48f0d8eb5cfc197b63d86957df340f2 +Subproject commit 6645eaf63fe7c44669f0d62ab95003bcf1d3d04d diff --git a/MareSynchronosServer/MareSynchronosServer/Controllers/JwtController.cs b/MareSynchronosServer/MareSynchronosServer/Controllers/JwtController.cs new file mode 100644 index 0000000..966419b --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Controllers/JwtController.cs @@ -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 _configuration; + + public JwtController(IHttpContextAccessor accessor, SecretKeyAuthenticatorService secretKeyAuthenticatorService, IConfigurationService configuration) + { + _accessor = accessor; + _secretKeyAuthenticatorService = secretKeyAuthenticatorService; + _configuration = configuration; + } + + [AllowAnonymous] + [HttpPost(MareAuth.AuthCreate)] + public async Task 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() + { + new Claim(ClaimTypes.NameIdentifier, authResult.Uid) + }); + + return Content(token.RawData); + } + + private JwtSecurityToken CreateToken(IEnumerable authClaims) + { + var authSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration.GetValue(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); + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs index 6f4fc66..73fe814 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Files.cs @@ -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); diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs index c3951e4..c24f0ba 100644 --- a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs @@ -14,6 +14,7 @@ using Microsoft.EntityFrameworkCore; namespace MareSynchronosServer.Hubs; +[Authorize(Policy = "Authenticated")] public partial class MareHub : Hub, IMareHub { private readonly MareMetrics _mareMetrics; @@ -118,6 +119,7 @@ public partial class MareHub : Hub, 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 await base.OnConnectedAsync().ConfigureAwait(false); } + [Authorize(Policy = "Authenticated")] public override async Task OnDisconnectedAsync(Exception exception) { _mareMetrics.DecGauge(MetricsAPI.GaugeConnections); diff --git a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj index d70749f..a24a34d 100644 --- a/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj +++ b/MareSynchronosServer/MareSynchronosServer/MareSynchronosServer.csproj @@ -31,11 +31,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/MareSynchronosServer/MareSynchronosServer/Startup.cs b/MareSynchronosServer/MareSynchronosServer/Startup.cs index 48abe0a..6abc6dd 100644 --- a/MareSynchronosServer/MareSynchronosServer/Startup.cs +++ b/MareSynchronosServer/MareSynchronosServer/Startup.cs @@ -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(); services.AddTransient(); - services.AddAuthentication(SecretKeyAuthenticationHandler.AuthScheme) - .AddScheme(SecretKeyAuthenticationHandler.AuthScheme, options => { options.Validate(); }); + + services.AddOptions(JwtBearerDefaults.AuthenticationScheme) + .Configure>((o, s) => + { + o.TokenValidationParameters = new() + { + ValidateIssuer = false, + ValidateLifetime = false, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(s.GetValue(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>().AllowAnonymous(); } - endpoints.MapHealthChecks("/health").WithMetadata(new AllowAnonymousAttribute()); + endpoints.MapHealthChecks("/health").AllowAnonymous(); + endpoints.MapControllers().AllowAnonymous(); }); } } diff --git a/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthReply.cs b/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthReply.cs index 49adf23..bfe3995 100644 --- a/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthReply.cs +++ b/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthReply.cs @@ -1,3 +1,3 @@ namespace MareSynchronosShared.Authentication; -internal record SecretKeyAuthReply(bool Success, string? Uid); +public record SecretKeyAuthReply(bool Success, string? Uid); diff --git a/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthenticationHandler.cs b/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthenticationHandler.cs deleted file mode 100644 index acd24c4..0000000 --- a/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthenticationHandler.cs +++ /dev/null @@ -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 -{ - public const string AuthScheme = "SecretKeyGrpcAuth"; - - private readonly IHttpContextAccessor _accessor; - private readonly SecretKeyAuthenticatorService secretKeyAuthenticatorService; - private static readonly ConcurrentDictionary IPLocks = new(StringComparer.Ordinal); - - public SecretKeyAuthenticationHandler(IHttpContextAccessor accessor, SecretKeyAuthenticatorService secretKeyAuthenticatorService, - IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) - { - _accessor = accessor; - this.secretKeyAuthenticatorService = secretKeyAuthenticatorService; - } - - protected override async Task HandleAuthenticateAsync() - { - var endpoint = Context.GetEndpoint(); - if (endpoint?.Metadata?.GetMetadata() != 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 - { - 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(); - } - } -} diff --git a/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthenticatorService.cs b/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthenticatorService.cs index 1920bf8..697209d 100644 --- a/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthenticatorService.cs +++ b/MareSynchronosServer/MareSynchronosShared/Authentication/SecretKeyAuthenticatorService.cs @@ -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 AuthorizeAsync(string ip, string secretKey) + public async Task 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(); - 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 _); }); } diff --git a/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationAuthBase.cs b/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationAuthBase.cs index 720ff45..b14c47e 100644 --- a/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationAuthBase.cs +++ b/MareSynchronosServer/MareSynchronosShared/Utils/MareConfigurationAuthBase.cs @@ -11,6 +11,8 @@ public class MareConfigurationAuthBase : MareConfigurationBase public int TempBanDurationInMinutes { get; set; } = 5; [RemoteConfiguration] public List 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(); } diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/GrpcFileService.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/GrpcFileService.cs index 624a2f0..855618b 100644 --- a/MareSynchronosServer/MareSynchronosStaticFilesServer/GrpcFileService.cs +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/GrpcFileService.cs @@ -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(); } } diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj b/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj index 05180b9..fc39033 100644 --- a/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/MareSynchronosStaticFilesServer.csproj @@ -24,6 +24,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs b/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs index 00597b5..120b589 100644 --- a/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs +++ b/MareSynchronosServer/MareSynchronosStaticFilesServer/Startup.cs @@ -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(JwtBearerDefaults.AuthenticationScheme) + .Configure>((o, s) => { - options.DefaultScheme = SecretKeyAuthenticationHandler.AuthScheme; - }) - .AddScheme(SecretKeyAuthenticationHandler.AuthScheme, options => { }); + o.TokenValidationParameters = new() + { + ValidateIssuer = false, + ValidateLifetime = false, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(s.GetValue(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)