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:
rootdarkarchon
2023-01-02 17:07:34 +01:00
committed by GitHub
parent bdd8830c8e
commit 5f0c12ecfa
15 changed files with 140 additions and 101 deletions

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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>

View File

@@ -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();
});
}
}

View File

@@ -1,3 +1,3 @@
namespace MareSynchronosShared.Authentication;
internal record SecretKeyAuthReply(bool Success, string? Uid);
public record SecretKeyAuthReply(bool Success, string? Uid);

View File

@@ -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();
}
}
}

View File

@@ -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 _);
});
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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)