Initial
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
using System.Collections.Concurrent;
|
||||
using MareSynchronos.API.Dto.Account;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils;
|
||||
using MareSynchronosShared.Utils.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.RegularExpressions;
|
||||
using MareSynchronosShared.Models;
|
||||
|
||||
namespace MareSynchronosAuthService.Services;
|
||||
|
||||
internal record IpRegistrationCount
|
||||
{
|
||||
private int count = 1;
|
||||
public int Count => count;
|
||||
public Task ResetTask { get; set; }
|
||||
public CancellationTokenSource ResetTaskCts { get; set; }
|
||||
public void IncreaseCount()
|
||||
{
|
||||
Interlocked.Increment(ref count);
|
||||
}
|
||||
}
|
||||
|
||||
public class AccountRegistrationService
|
||||
{
|
||||
private readonly MareMetrics _metrics;
|
||||
private readonly MareDbContext _mareDbContext;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IConfigurationService<AuthServiceConfiguration> _configurationService;
|
||||
private readonly ILogger<AccountRegistrationService> _logger;
|
||||
private readonly ConcurrentDictionary<string, IpRegistrationCount> _registrationsPerIp = new(StringComparer.Ordinal);
|
||||
|
||||
private Regex _registrationUserAgentRegex = new Regex(@"^MareSynchronos/", RegexOptions.Compiled);
|
||||
|
||||
public AccountRegistrationService(MareMetrics metrics, MareDbContext mareDbContext,
|
||||
IServiceScopeFactory serviceScopeFactory, IConfigurationService<AuthServiceConfiguration> configuration,
|
||||
ILogger<AccountRegistrationService> logger)
|
||||
{
|
||||
_mareDbContext = mareDbContext;
|
||||
_logger = logger;
|
||||
_configurationService = configuration;
|
||||
_metrics = metrics;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public async Task<RegisterReplyV2Dto> RegisterAccountAsync(string ua, string ip, string hashedSecretKey)
|
||||
{
|
||||
var reply = new RegisterReplyV2Dto();
|
||||
|
||||
if (!_registrationUserAgentRegex.Match(ua).Success)
|
||||
{
|
||||
reply.ErrorMessage = "User-Agent not allowed";
|
||||
return reply;
|
||||
}
|
||||
|
||||
if (_registrationsPerIp.TryGetValue(ip, out var registrationCount)
|
||||
&& registrationCount.Count >= _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpLimit), 3))
|
||||
{
|
||||
_logger.LogWarning("Rejecting {ip} for registration spam", ip);
|
||||
|
||||
if (registrationCount.ResetTask == null)
|
||||
{
|
||||
registrationCount.ResetTaskCts = new CancellationTokenSource();
|
||||
|
||||
if (registrationCount.ResetTaskCts != null)
|
||||
registrationCount.ResetTaskCts.Cancel();
|
||||
|
||||
registrationCount.ResetTask = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpDurationInMinutes), 10))).ConfigureAwait(false);
|
||||
|
||||
}).ContinueWith((t) =>
|
||||
{
|
||||
_registrationsPerIp.Remove(ip, out _);
|
||||
}, registrationCount.ResetTaskCts.Token);
|
||||
}
|
||||
reply.ErrorMessage = "Too many registrations from this IP. Please try again later.";
|
||||
return reply;
|
||||
}
|
||||
|
||||
var user = new User();
|
||||
|
||||
var hasValidUid = false;
|
||||
while (!hasValidUid)
|
||||
{
|
||||
var uid = StringUtils.GenerateRandomString(7);
|
||||
if (_mareDbContext.Users.Any(u => u.UID == uid || u.Alias == uid)) continue;
|
||||
user.UID = uid;
|
||||
hasValidUid = true;
|
||||
}
|
||||
|
||||
// make the first registered user on the service to admin
|
||||
if (!await _mareDbContext.Users.AnyAsync().ConfigureAwait(false))
|
||||
{
|
||||
user.IsAdmin = true;
|
||||
}
|
||||
|
||||
user.LastLoggedIn = DateTime.UtcNow;
|
||||
|
||||
var auth = new Auth()
|
||||
{
|
||||
HashedKey = hashedSecretKey,
|
||||
User = user,
|
||||
};
|
||||
|
||||
await _mareDbContext.Users.AddAsync(user).ConfigureAwait(false);
|
||||
await _mareDbContext.Auth.AddAsync(auth).ConfigureAwait(false);
|
||||
await _mareDbContext.SaveChangesAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("User registered: {userUID} from IP {ip}", user.UID, ip);
|
||||
_metrics.IncCounter(MetricsAPI.CounterAccountsCreated);
|
||||
|
||||
reply.Success = true;
|
||||
reply.UID = user.UID;
|
||||
|
||||
RecordIpRegistration(ip);
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
private void RecordIpRegistration(string ip)
|
||||
{
|
||||
var whitelisted = _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.WhitelistedIps), new List<string>());
|
||||
if (!whitelisted.Any(w => ip.Contains(w, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (_registrationsPerIp.TryGetValue(ip, out var count))
|
||||
{
|
||||
count.IncreaseCount();
|
||||
}
|
||||
else
|
||||
{
|
||||
count = _registrationsPerIp[ip] = new IpRegistrationCount();
|
||||
|
||||
if (count.ResetTaskCts != null)
|
||||
count.ResetTaskCts.Cancel();
|
||||
|
||||
count.ResetTaskCts = new CancellationTokenSource();
|
||||
|
||||
count.ResetTask = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.RegisterIpDurationInMinutes), 10))).ConfigureAwait(false);
|
||||
|
||||
}).ContinueWith((t) =>
|
||||
{
|
||||
_registrationsPerIp.Remove(ip, out _);
|
||||
}, count.ResetTaskCts.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,138 @@
|
||||
using MareSynchronosShared;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils.Configuration;
|
||||
using MaxMind.GeoIP2;
|
||||
|
||||
namespace MareSynchronosAuthService.Services;
|
||||
|
||||
public class GeoIPService : IHostedService
|
||||
{
|
||||
private readonly ILogger<GeoIPService> _logger;
|
||||
private readonly IConfigurationService<AuthServiceConfiguration> _mareConfiguration;
|
||||
private bool _useGeoIP = false;
|
||||
private string _cityFile = string.Empty;
|
||||
private DatabaseReader? _dbReader;
|
||||
private DateTime _dbLastWriteTime = DateTime.Now;
|
||||
private CancellationTokenSource _fileWriteTimeCheckCts = new();
|
||||
private bool _processingReload = false;
|
||||
|
||||
public GeoIPService(ILogger<GeoIPService> logger,
|
||||
IConfigurationService<AuthServiceConfiguration> mareConfiguration)
|
||||
{
|
||||
_logger = logger;
|
||||
_mareConfiguration = mareConfiguration;
|
||||
}
|
||||
|
||||
public async Task<string> GetCountryFromIP(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
if (!_useGeoIP)
|
||||
{
|
||||
return "*";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var ip = httpContextAccessor.GetIpAddress();
|
||||
|
||||
using CancellationTokenSource waitCts = new();
|
||||
waitCts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
while (_processingReload) await Task.Delay(100, waitCts.Token).ConfigureAwait(false);
|
||||
|
||||
if (_dbReader!.TryCity(ip, out var response))
|
||||
{
|
||||
string? continent = response?.Continent.Code;
|
||||
if (!string.IsNullOrEmpty(continent) &&
|
||||
string.Equals(continent, "NA", StringComparison.Ordinal)
|
||||
&& response?.Location.Longitude != null)
|
||||
{
|
||||
if (response.Location.Longitude < -102)
|
||||
{
|
||||
continent = "NA-W";
|
||||
}
|
||||
else
|
||||
{
|
||||
continent = "NA-E";
|
||||
}
|
||||
}
|
||||
|
||||
return continent ?? "*";
|
||||
}
|
||||
|
||||
return "*";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error handling Geo IP country in request");
|
||||
return "*";
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("GeoIP module starting update task");
|
||||
|
||||
var token = _fileWriteTimeCheckCts.Token;
|
||||
_ = PeriodicReloadTask(token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task PeriodicReloadTask(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_processingReload = true;
|
||||
|
||||
var useGeoIP = _mareConfiguration.GetValueOrDefault(nameof(AuthServiceConfiguration.UseGeoIP), false);
|
||||
var cityFile = _mareConfiguration.GetValueOrDefault(nameof(AuthServiceConfiguration.GeoIPDbCityFile), string.Empty);
|
||||
var lastWriteTime = new FileInfo(cityFile).LastWriteTimeUtc;
|
||||
if (useGeoIP && (!string.Equals(cityFile, _cityFile, StringComparison.OrdinalIgnoreCase) || lastWriteTime != _dbLastWriteTime))
|
||||
{
|
||||
_cityFile = cityFile;
|
||||
if (!File.Exists(_cityFile)) throw new FileNotFoundException($"Could not open GeoIP City Database, path does not exist: {_cityFile}");
|
||||
_dbReader?.Dispose();
|
||||
_dbReader = null;
|
||||
_dbReader = new DatabaseReader(_cityFile);
|
||||
_dbLastWriteTime = lastWriteTime;
|
||||
|
||||
_ = _dbReader.City("8.8.8.8").Continent;
|
||||
|
||||
_logger.LogInformation($"Loaded GeoIP city file from {_cityFile}");
|
||||
|
||||
if (_useGeoIP != useGeoIP)
|
||||
{
|
||||
_logger.LogInformation("GeoIP module is now enabled");
|
||||
_useGeoIP = useGeoIP;
|
||||
}
|
||||
}
|
||||
|
||||
if (_useGeoIP != useGeoIP && !useGeoIP)
|
||||
{
|
||||
_logger.LogInformation("GeoIP module is now disabled");
|
||||
_useGeoIP = useGeoIP;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Error during periodic GeoIP module reload task, disabling GeoIP");
|
||||
_useGeoIP = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_processingReload = false;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_fileWriteTimeCheckCts.Cancel();
|
||||
_fileWriteTimeCheckCts.Dispose();
|
||||
_dbReader?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
using System.Collections.Concurrent;
|
||||
using MareSynchronosAuthService.Authentication;
|
||||
using MareSynchronosShared.Data;
|
||||
using MareSynchronosShared.Metrics;
|
||||
using MareSynchronosShared.Services;
|
||||
using MareSynchronosShared.Utils.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MareSynchronosAuthService.Services;
|
||||
|
||||
public class SecretKeyAuthenticatorService
|
||||
{
|
||||
private readonly MareMetrics _metrics;
|
||||
private readonly MareDbContext _mareDbContext;
|
||||
private readonly IConfigurationService<AuthServiceConfiguration> _configurationService;
|
||||
private readonly ILogger<SecretKeyAuthenticatorService> _logger;
|
||||
private readonly ConcurrentDictionary<string, SecretKeyFailedAuthorization> _failedAuthorizations = new(StringComparer.Ordinal);
|
||||
|
||||
public SecretKeyAuthenticatorService(MareMetrics metrics, MareDbContext mareDbContext,
|
||||
IConfigurationService<AuthServiceConfiguration> configuration, ILogger<SecretKeyAuthenticatorService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_configurationService = configuration;
|
||||
_metrics = metrics;
|
||||
_mareDbContext = mareDbContext;
|
||||
}
|
||||
|
||||
public async Task<SecretKeyAuthReply> AuthorizeAsync(string ip, string hashedSecretKey)
|
||||
{
|
||||
_metrics.IncCounter(MetricsAPI.CounterAuthenticationRequests);
|
||||
|
||||
if (_failedAuthorizations.TryGetValue(ip, out var existingFailedAuthorization)
|
||||
&& existingFailedAuthorization.FailedAttempts > _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.FailedAuthForTempBan), 5))
|
||||
{
|
||||
if (existingFailedAuthorization.ResetTask == null)
|
||||
{
|
||||
_logger.LogWarning("TempBan {ip} for authorization spam", ip);
|
||||
|
||||
existingFailedAuthorization.ResetTask = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(_configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.TempBanDurationInMinutes), 5))).ConfigureAwait(false);
|
||||
|
||||
}).ContinueWith((t) =>
|
||||
{
|
||||
_failedAuthorizations.Remove(ip, out _);
|
||||
});
|
||||
}
|
||||
return new(Success: false, Uid: null, TempBan: true, Alias: null, Permaban: false);
|
||||
}
|
||||
|
||||
var authReply = await _mareDbContext.Auth.Include(a => a.User).AsNoTracking()
|
||||
.SingleOrDefaultAsync(u => u.HashedKey == hashedSecretKey).ConfigureAwait(false);
|
||||
|
||||
SecretKeyAuthReply reply = new(authReply != null, authReply?.UserUID, authReply?.User?.Alias ?? string.Empty, TempBan: false, authReply?.IsBanned ?? false);
|
||||
|
||||
if (reply.Success)
|
||||
{
|
||||
_metrics.IncCounter(MetricsAPI.CounterAuthenticationSuccesses);
|
||||
}
|
||||
else
|
||||
{
|
||||
return AuthenticationFailure(ip);
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
private SecretKeyAuthReply AuthenticationFailure(string ip)
|
||||
{
|
||||
_metrics.IncCounter(MetricsAPI.CounterAuthenticationFailures);
|
||||
|
||||
_logger.LogWarning("Failed authorization from {ip}", ip);
|
||||
var whitelisted = _configurationService.GetValueOrDefault(nameof(AuthServiceConfiguration.WhitelistedIps), new List<string>());
|
||||
if (!whitelisted.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, Alias: null, TempBan: false, Permaban: false);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user