Account registration endpoint
This commit is contained in:
@@ -0,0 +1,153 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using MareSynchronos.API.Dto.Account;
|
||||||
|
using MareSynchronosShared.Data;
|
||||||
|
using MareSynchronosShared.Metrics;
|
||||||
|
using MareSynchronosShared.Services;
|
||||||
|
using MareSynchronosShared.Utils;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using MareSynchronosShared.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronosServer.Authentication;
|
||||||
|
|
||||||
|
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<MareConfigurationAuthBase> _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<MareConfigurationAuthBase> configuration,
|
||||||
|
ILogger<AccountRegistrationService> logger)
|
||||||
|
{
|
||||||
|
_mareDbContext = mareDbContext;
|
||||||
|
_logger = logger;
|
||||||
|
_configurationService = configuration;
|
||||||
|
_metrics = metrics;
|
||||||
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<RegisterReplyDto> RegisterAccountAsync(string ua, string ip)
|
||||||
|
{
|
||||||
|
var reply = new RegisterReplyDto();
|
||||||
|
|
||||||
|
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(MareConfigurationAuthBase.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(MareConfigurationAuthBase.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 computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
|
||||||
|
var auth = new Auth()
|
||||||
|
{
|
||||||
|
HashedKey = StringUtils.Sha256String(computedHash),
|
||||||
|
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.CounterAuthenticationRequests);
|
||||||
|
|
||||||
|
reply.Success = true;
|
||||||
|
reply.UID = user.UID;
|
||||||
|
reply.SecretKey = computedHash;
|
||||||
|
|
||||||
|
RecordIpRegistration(ip);
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecordIpRegistration(string ip)
|
||||||
|
{
|
||||||
|
var whitelisted = _configurationService.GetValueOrDefault(nameof(MareConfigurationAuthBase.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(MareConfigurationAuthBase.RegisterIpDurationInMinutes), 10))).ConfigureAwait(false);
|
||||||
|
|
||||||
|
}).ContinueWith((t) =>
|
||||||
|
{
|
||||||
|
_registrationsPerIp.Remove(ip, out _);
|
||||||
|
}, count.ResetTaskCts.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,10 +24,12 @@ public class JwtController : Controller
|
|||||||
private readonly IRedisDatabase _redis;
|
private readonly IRedisDatabase _redis;
|
||||||
private readonly MareDbContext _mareDbContext;
|
private readonly MareDbContext _mareDbContext;
|
||||||
private readonly SecretKeyAuthenticatorService _secretKeyAuthenticatorService;
|
private readonly SecretKeyAuthenticatorService _secretKeyAuthenticatorService;
|
||||||
|
private readonly AccountRegistrationService _accountRegistrationService;
|
||||||
private readonly IConfigurationService<MareConfigurationAuthBase> _configuration;
|
private readonly IConfigurationService<MareConfigurationAuthBase> _configuration;
|
||||||
|
|
||||||
public JwtController(IHttpContextAccessor accessor, MareDbContext mareDbContext,
|
public JwtController(IHttpContextAccessor accessor, MareDbContext mareDbContext,
|
||||||
SecretKeyAuthenticatorService secretKeyAuthenticatorService,
|
SecretKeyAuthenticatorService secretKeyAuthenticatorService,
|
||||||
|
AccountRegistrationService accountRegistrationService,
|
||||||
IConfigurationService<MareConfigurationAuthBase> configuration,
|
IConfigurationService<MareConfigurationAuthBase> configuration,
|
||||||
IRedisDatabase redisDb)
|
IRedisDatabase redisDb)
|
||||||
{
|
{
|
||||||
@@ -35,6 +37,7 @@ public class JwtController : Controller
|
|||||||
_redis = redisDb;
|
_redis = redisDb;
|
||||||
_mareDbContext = mareDbContext;
|
_mareDbContext = mareDbContext;
|
||||||
_secretKeyAuthenticatorService = secretKeyAuthenticatorService;
|
_secretKeyAuthenticatorService = secretKeyAuthenticatorService;
|
||||||
|
_accountRegistrationService = accountRegistrationService;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +117,15 @@ public class JwtController : Controller
|
|||||||
return Content(token.RawData);
|
return Content(token.RawData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost(MareAuth.Auth_Register)]
|
||||||
|
public async Task<IActionResult> Register()
|
||||||
|
{
|
||||||
|
var ua = HttpContext.Request.Headers["User-Agent"][0] ?? "-";
|
||||||
|
var ip = _accessor.GetIpAddress();
|
||||||
|
return Json(await _accountRegistrationService.RegisterAccountAsync(ua, ip));
|
||||||
|
}
|
||||||
|
|
||||||
private JwtSecurityToken CreateToken(IEnumerable<Claim> authClaims)
|
private JwtSecurityToken CreateToken(IEnumerable<Claim> authClaims)
|
||||||
{
|
{
|
||||||
var authSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration.GetValue<string>(nameof(MareConfigurationAuthBase.Jwt))));
|
var authSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration.GetValue<string>(nameof(MareConfigurationAuthBase.Jwt))));
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ public class Startup
|
|||||||
private static void ConfigureAuthorization(IServiceCollection services)
|
private static void ConfigureAuthorization(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<SecretKeyAuthenticatorService>();
|
services.AddSingleton<SecretKeyAuthenticatorService>();
|
||||||
|
services.AddSingleton<AccountRegistrationService>();
|
||||||
services.AddTransient<IAuthorizationHandler, UserRequirementHandler>();
|
services.AddTransient<IAuthorizationHandler, UserRequirementHandler>();
|
||||||
|
|
||||||
services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
|
services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
|
||||||
@@ -257,6 +258,7 @@ public class Startup
|
|||||||
MetricsAPI.CounterAuthenticationFailures,
|
MetricsAPI.CounterAuthenticationFailures,
|
||||||
MetricsAPI.CounterAuthenticationRequests,
|
MetricsAPI.CounterAuthenticationRequests,
|
||||||
MetricsAPI.CounterAuthenticationSuccesses,
|
MetricsAPI.CounterAuthenticationSuccesses,
|
||||||
|
MetricsAPI.CounterAccountsCreated,
|
||||||
}, new List<string>
|
}, new List<string>
|
||||||
{
|
{
|
||||||
MetricsAPI.GaugeAuthorizedConnections,
|
MetricsAPI.GaugeAuthorizedConnections,
|
||||||
|
|||||||
@@ -33,4 +33,5 @@ public class MetricsAPI
|
|||||||
public const string GaugeDownloadQueue = "mare_download_queue";
|
public const string GaugeDownloadQueue = "mare_download_queue";
|
||||||
public const string CounterFileRequests = "mare_files_requests";
|
public const string CounterFileRequests = "mare_files_requests";
|
||||||
public const string CounterFileRequestSize = "mare_files_request_size";
|
public const string CounterFileRequestSize = "mare_files_request_size";
|
||||||
|
public const string CounterAccountsCreated = "mare_accounts_created";
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,10 @@ public class MareConfigurationAuthBase : MareConfigurationBase
|
|||||||
[RemoteConfiguration]
|
[RemoteConfiguration]
|
||||||
public int TempBanDurationInMinutes { get; set; } = 5;
|
public int TempBanDurationInMinutes { get; set; } = 5;
|
||||||
[RemoteConfiguration]
|
[RemoteConfiguration]
|
||||||
|
public int RegisterIpLimit { get; set; } = 3;
|
||||||
|
[RemoteConfiguration]
|
||||||
|
public int RegisterIpDurationInMinutes { get; set; } = 10;
|
||||||
|
[RemoteConfiguration]
|
||||||
public List<string> WhitelistedIps { get; set; } = new();
|
public List<string> WhitelistedIps { get; set; } = new();
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
@@ -17,6 +21,8 @@ public class MareConfigurationAuthBase : MareConfigurationBase
|
|||||||
sb.AppendLine(base.ToString());
|
sb.AppendLine(base.ToString());
|
||||||
sb.AppendLine($"{nameof(FailedAuthForTempBan)} => {FailedAuthForTempBan}");
|
sb.AppendLine($"{nameof(FailedAuthForTempBan)} => {FailedAuthForTempBan}");
|
||||||
sb.AppendLine($"{nameof(TempBanDurationInMinutes)} => {TempBanDurationInMinutes}");
|
sb.AppendLine($"{nameof(TempBanDurationInMinutes)} => {TempBanDurationInMinutes}");
|
||||||
|
sb.AppendLine($"{nameof(RegisterIpLimit)} => {RegisterIpLimit}");
|
||||||
|
sb.AppendLine($"{nameof(RegisterIpDurationInMinutes)} => {RegisterIpDurationInMinutes}");
|
||||||
sb.AppendLine($"{nameof(Jwt)} => {Jwt}");
|
sb.AppendLine($"{nameof(Jwt)} => {Jwt}");
|
||||||
sb.AppendLine($"{nameof(WhitelistedIps)} => {string.Join(", ", WhitelistedIps)}");
|
sb.AppendLine($"{nameof(WhitelistedIps)} => {string.Join(", ", WhitelistedIps)}");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
|
|||||||
Reference in New Issue
Block a user