rework authentication
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
namespace MareSynchronosShared.Authentication;
|
||||
|
||||
internal record SecretKeyAuthReply(bool Success, string? Uid);
|
||||
@@ -1,27 +1,27 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using MareSynchronosServer;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Data;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ISystemClock = Microsoft.AspNetCore.Authentication.ISystemClock;
|
||||
|
||||
namespace MareSynchronosShared.Authentication;
|
||||
|
||||
public class SecretKeyGrpcAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
public class SecretKeyAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
public const string AuthScheme = "SecretKeyGrpcAuth";
|
||||
|
||||
private readonly GrpcAuthenticationService _grpcAuthService;
|
||||
private readonly MareDbContext _mareDbContext;
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
private readonly SecretKeyAuthenticatorService secretKeyAuthenticatorService;
|
||||
|
||||
public SecretKeyGrpcAuthenticationHandler(IHttpContextAccessor accessor, GrpcAuthenticationService authClient,
|
||||
public SecretKeyAuthenticationHandler(IHttpContextAccessor accessor, SecretKeyAuthenticatorService secretKeyAuthenticatorService,
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
|
||||
{
|
||||
this._grpcAuthService = authClient;
|
||||
_accessor = accessor;
|
||||
this.secretKeyAuthenticatorService = secretKeyAuthenticatorService;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
@@ -33,22 +33,20 @@ public class SecretKeyGrpcAuthenticationHandler : AuthenticationHandler<Authenti
|
||||
|
||||
var ip = _accessor.GetIpAddress();
|
||||
|
||||
var authResult = await _grpcAuthService.AuthorizeAsync(ip, authHeader).ConfigureAwait(false);
|
||||
var authResult = await secretKeyAuthenticatorService.AuthorizeAsync(ip, authHeader).ConfigureAwait(false);
|
||||
|
||||
if (!authResult.Success)
|
||||
{
|
||||
return AuthenticateResult.Fail("Failed Authorization");
|
||||
}
|
||||
|
||||
var uid = authResult.Uid;
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, uid.Uid),
|
||||
new(ClaimTypes.NameIdentifier, authResult.Uid),
|
||||
new(ClaimTypes.Authentication, authHeader)
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, nameof(SecretKeyGrpcAuthenticationHandler));
|
||||
var identity = new ClaimsIdentity(claims, nameof(SecretKeyAuthenticationHandler));
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
using System.Collections.Concurrent;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronosShared.Authentication;
|
||||
|
||||
public class SecretKeyAuthenticatorService
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ILogger<SecretKeyAuthenticatorService> _logger;
|
||||
private readonly ConcurrentDictionary<string, SecretKeyAuthReply> _cachedPositiveResponses = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, SecretKeyFailedAuthorization?> _failedAuthorizations = new(StringComparer.Ordinal);
|
||||
private readonly int _failedAttemptsForTempBan;
|
||||
private readonly int _tempBanMinutes;
|
||||
private readonly List<string> _whitelistedIps;
|
||||
|
||||
public SecretKeyAuthenticatorService(IServiceScopeFactory serviceScopeFactory, IConfiguration configuration, ILogger<SecretKeyAuthenticatorService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
var config = configuration.GetRequiredSection("MareSynchronos");
|
||||
_failedAttemptsForTempBan = config.GetValue<int>("FailedAuthForTempBan", 5);
|
||||
logger.LogInformation("FailedAuthForTempBan: {num}", _failedAttemptsForTempBan);
|
||||
_tempBanMinutes = config.GetValue<int>("TempBanDurationInMinutes", 30);
|
||||
logger.LogInformation("TempBanMinutes: {num}", _tempBanMinutes);
|
||||
_whitelistedIps = config.GetSection("WhitelistedIps").Get<List<string>>();
|
||||
foreach (var ip in _whitelistedIps)
|
||||
{
|
||||
logger.LogInformation("Whitelisted IP: " + ip);
|
||||
}
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
internal async Task<SecretKeyAuthReply> AuthorizeAsync(string ip, string secretKey)
|
||||
{
|
||||
if (_cachedPositiveResponses.TryGetValue(secretKey, out var cachedPositiveResponse))
|
||||
{
|
||||
return cachedPositiveResponse;
|
||||
}
|
||||
|
||||
if (_failedAuthorizations.TryGetValue(ip, out var existingFailedAuthorization) && existingFailedAuthorization.FailedAttempts > _failedAttemptsForTempBan)
|
||||
{
|
||||
if (existingFailedAuthorization.ResetTask == null)
|
||||
{
|
||||
_logger.LogWarning("TempBan {ip} for authorization spam", ip);
|
||||
|
||||
existingFailedAuthorization.ResetTask = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(_tempBanMinutes)).ConfigureAwait(false);
|
||||
|
||||
}).ContinueWith((t) =>
|
||||
{
|
||||
_failedAuthorizations.Remove(ip, out _);
|
||||
});
|
||||
}
|
||||
return new(Success: false, Uid: null);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
SecretKeyAuthReply reply = new(authReply != null, authReply?.UserUID);
|
||||
|
||||
if (reply.Success)
|
||||
{
|
||||
_cachedPositiveResponses[secretKey] = reply;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(5)).ConfigureAwait(false);
|
||||
_cachedPositiveResponses.TryRemove(secretKey, out _);
|
||||
});
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
return AuthenticationFailure(ip);
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
private SecretKeyAuthReply AuthenticationFailure(string ip)
|
||||
{
|
||||
_logger.LogWarning("Failed authorization from {ip}", ip);
|
||||
if (!_whitelistedIps.Any(w => ip.Contains(w, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (_failedAuthorizations.TryGetValue(ip, out var auth))
|
||||
{
|
||||
auth.IncreaseFailedAttempts();
|
||||
}
|
||||
else
|
||||
{
|
||||
_failedAuthorizations[ip] = new SecretKeyFailedAuthorization();
|
||||
}
|
||||
}
|
||||
|
||||
return new(Success: false, Uid: null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace MareSynchronosShared.Authentication;
|
||||
|
||||
internal record SecretKeyFailedAuthorization
|
||||
{
|
||||
private int failedAttempts = 1;
|
||||
public int FailedAttempts => failedAttempts;
|
||||
public Task ResetTask { get; set; }
|
||||
public void IncreaseFailedAttempts()
|
||||
{
|
||||
Interlocked.Increment(ref failedAttempts);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,6 @@ option csharp_namespace = "MareSynchronosShared.Protos";
|
||||
|
||||
package mareservices;
|
||||
|
||||
service AuthService {
|
||||
rpc Authorize (stream AuthRequest) returns (stream AuthReply);
|
||||
rpc RemoveAuth (UidMessage) returns (Empty);
|
||||
rpc ClearUnauthorized (Empty) returns (Empty);
|
||||
}
|
||||
|
||||
service FileService {
|
||||
rpc UploadFile (stream UploadFileRequest) returns (Empty);
|
||||
rpc GetFileSizes (FileSizeRequest) returns (FileSizeResponse);
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using MareSynchronosShared.Protos;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronosShared.Services;
|
||||
|
||||
public class GrpcAuthenticationService : GrpcBaseService
|
||||
{
|
||||
private record AuthRequestInternal
|
||||
{
|
||||
public AuthRequest Request { get; set; }
|
||||
public long Id { get; set; }
|
||||
}
|
||||
|
||||
private record AuthResponseCache
|
||||
{
|
||||
public AuthReply Response { get; set; }
|
||||
public DateTime WrittenTo { get; set; }
|
||||
}
|
||||
|
||||
private readonly AuthService.AuthServiceClient _authClient;
|
||||
private readonly ConcurrentQueue<AuthRequestInternal> _requestQueue = new();
|
||||
private readonly ConcurrentDictionary<long, AuthReply> _authReplies = new();
|
||||
private readonly ConcurrentDictionary<string, AuthResponseCache> _cachedPositiveResponses = new(StringComparer.Ordinal);
|
||||
private long _requestId = 0;
|
||||
|
||||
public GrpcAuthenticationService(ILogger<GrpcAuthenticationService> logger, AuthService.AuthServiceClient authClient) : base(logger)
|
||||
{
|
||||
_authClient = authClient;
|
||||
}
|
||||
|
||||
public async Task<AuthReply> AuthorizeAsync(string ip, string secretKey)
|
||||
{
|
||||
if (_cachedPositiveResponses.TryGetValue(secretKey, out var cachedPositiveResponse))
|
||||
{
|
||||
if (cachedPositiveResponse.WrittenTo.AddMinutes(5) < DateTime.UtcNow) return cachedPositiveResponse.Response;
|
||||
_cachedPositiveResponses.Remove(secretKey, out _);
|
||||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _requestId);
|
||||
_requestQueue.Enqueue(new AuthRequestInternal()
|
||||
{
|
||||
Id = id,
|
||||
Request = new AuthRequest()
|
||||
{
|
||||
Ip = ip,
|
||||
SecretKey = secretKey,
|
||||
}
|
||||
});
|
||||
|
||||
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
|
||||
AuthReply response = null;
|
||||
|
||||
while (!GrpcIsFaulty && !cts.IsCancellationRequested && !_authReplies.TryRemove(id, out response))
|
||||
{
|
||||
await Task.Delay(10, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (response?.Success ?? false)
|
||||
{
|
||||
_cachedPositiveResponses[secretKey] = new AuthResponseCache
|
||||
{
|
||||
Response = response,
|
||||
WrittenTo = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
return response ?? new AuthReply
|
||||
{
|
||||
Success = false,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task GrpcAuthStream(CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = _authClient.Authorize(cancellationToken: token);
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
while (_requestQueue.TryDequeue(out var request))
|
||||
{
|
||||
await stream.RequestStream.WriteAsync(request.Request, token).ConfigureAwait(false);
|
||||
await stream.ResponseStream.MoveNext(token).ConfigureAwait(false);
|
||||
_authReplies[request.Id] = stream.ResponseStream.Current;
|
||||
}
|
||||
|
||||
await Task.Delay(10, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
SetGrpcFaulty();
|
||||
}
|
||||
}
|
||||
|
||||
protected override Task OnGrpcRestore()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task PostStartStream()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task PreStartStream()
|
||||
{
|
||||
_requestQueue.Clear();
|
||||
_authReplies.Clear();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task StartAsyncInternal(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task StartStream(CancellationToken ct)
|
||||
{
|
||||
_ = GrpcAuthStream(ct);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task StopAsyncInternal(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user