This commit is contained in:
2025-08-22 11:55:35 +01:00
commit b21d2a685e
312 changed files with 31174 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
namespace MareSynchronosAuthService.Authentication;
public record SecretKeyAuthReply(bool Success, string Uid, string Alias, bool TempBan, bool Permaban);

View File

@@ -0,0 +1,12 @@
namespace MareSynchronosAuthService.Authentication;
internal record SecretKeyFailedAuthorization
{
private int failedAttempts = 1;
public int FailedAttempts => failedAttempts;
public Task ResetTask { get; set; }
public void IncreaseFailedAttempts()
{
Interlocked.Increment(ref failedAttempts);
}
}

View File

@@ -0,0 +1,193 @@
using MareSynchronos.API.Dto;
using MareSynchronos.API.Dto.Account;
using MareSynchronos.API.Routes;
using MareSynchronosAuthService.Services;
using MareSynchronosShared;
using MareSynchronosShared.Data;
using MareSynchronosShared.Models;
using MareSynchronosShared.Services;
using MareSynchronosShared.Utils;
using MareSynchronosShared.Utils.Configuration;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using StackExchange.Redis.Extensions.Core.Abstractions;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace MareSynchronosAuthService.Controllers;
[AllowAnonymous]
[Route(MareAuth.Auth)]
public class JwtController : Controller
{
private readonly IHttpContextAccessor _accessor;
private readonly IRedisDatabase _redis;
private readonly IDbContextFactory<MareDbContext> _mareDbContextFactory;
private readonly GeoIPService _geoIPProvider;
private readonly SecretKeyAuthenticatorService _secretKeyAuthenticatorService;
private readonly AccountRegistrationService _accountRegistrationService;
private readonly IConfigurationService<AuthServiceConfiguration> _configuration;
public JwtController(ILogger<JwtController> logger,
IHttpContextAccessor accessor, IDbContextFactory<MareDbContext> mareDbContextFactory,
SecretKeyAuthenticatorService secretKeyAuthenticatorService,
AccountRegistrationService accountRegistrationService,
IConfigurationService<AuthServiceConfiguration> configuration,
IRedisDatabase redisDb, GeoIPService geoIPProvider)
{
_accessor = accessor;
_redis = redisDb;
_geoIPProvider = geoIPProvider;
_mareDbContextFactory = mareDbContextFactory;
_secretKeyAuthenticatorService = secretKeyAuthenticatorService;
_accountRegistrationService = accountRegistrationService;
_configuration = configuration;
}
[AllowAnonymous]
[HttpPost(MareAuth.Auth_CreateIdent)]
public async Task<IActionResult> CreateToken(string auth, string charaIdent)
{
if (string.IsNullOrEmpty(auth)) return BadRequest("No Authkey");
if (string.IsNullOrEmpty(charaIdent)) return BadRequest("No CharaIdent");
using var dbContext = await _mareDbContextFactory.CreateDbContextAsync();
var ip = _accessor.GetIpAddress();
var authResult = await _secretKeyAuthenticatorService.AuthorizeAsync(ip, auth);
var isBanned = await dbContext.BannedUsers.AsNoTracking().AnyAsync(u => u.CharacterIdentification == charaIdent).ConfigureAwait(false);
if (isBanned)
{
var authToBan = dbContext.Auth.SingleOrDefault(a => a.UserUID == authResult.Uid);
if (authToBan != null)
{
authToBan.IsBanned = true;
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
return Unauthorized("Your character is banned from using the service.");
}
if (!authResult.Success && !authResult.TempBan) return Unauthorized("The provided secret key is invalid. Verify your accounts existence and/or recover the secret key.");
if (!authResult.Success && authResult.TempBan) return Unauthorized("Due to an excessive amount of failed authentication attempts you are temporarily banned. Check your Secret Key configuration and try connecting again in 5 minutes.");
if (authResult.Permaban)
{
if (!dbContext.BannedUsers.Any(c => c.CharacterIdentification == charaIdent))
{
dbContext.BannedUsers.Add(new Banned()
{
CharacterIdentification = charaIdent,
Reason = "Autobanned CharacterIdent (" + authResult.Uid + ")",
});
await dbContext.SaveChangesAsync();
}
var lodestone = await dbContext.LodeStoneAuth.Include(a => a.User).FirstOrDefaultAsync(c => c.User.UID == authResult.Uid);
if (lodestone != null)
{
if (!dbContext.BannedRegistrations.Any(c => c.DiscordIdOrLodestoneAuth == lodestone.HashedLodestoneId))
{
dbContext.BannedRegistrations.Add(new BannedRegistrations()
{
DiscordIdOrLodestoneAuth = lodestone.HashedLodestoneId,
});
}
if (!dbContext.BannedRegistrations.Any(c => c.DiscordIdOrLodestoneAuth == lodestone.DiscordId.ToString()))
{
dbContext.BannedRegistrations.Add(new BannedRegistrations()
{
DiscordIdOrLodestoneAuth = lodestone.DiscordId.ToString(),
});
}
await dbContext.SaveChangesAsync();
}
return Unauthorized("You are permanently banned.");
}
var existingIdent = await _redis.GetAsync<string>("UID:" + authResult.Uid);
if (!string.IsNullOrEmpty(existingIdent)) return Unauthorized("Already logged in to this account. Reconnect in 60 seconds. If you keep seeing this issue, restart your game.");
var token = CreateToken(new List<Claim>()
{
new Claim(MareClaimTypes.Uid, authResult.Uid),
new Claim(MareClaimTypes.CharaIdent, charaIdent),
new Claim(MareClaimTypes.Alias, authResult.Alias),
new Claim(MareClaimTypes.Continent, await _geoIPProvider.GetCountryFromIP(_accessor)),
});
return Content(token.RawData);
}
[AllowAnonymous]
[HttpPost(MareAuth.Auth_CreateIdentV2)]
public async Task<IActionResult> CreateTokenV2(string auth, string charaIdent)
{
var tokenResponse = await CreateToken(auth, charaIdent);
var tokenContent = tokenResponse as ContentResult;
if (tokenContent == null)
return tokenResponse;
return Json(new AuthReplyDto
{
Token = tokenContent.Content,
WellKnown = _configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.WellKnown), string.Empty),
});
}
[AllowAnonymous]
[HttpPost(MareAuth.Auth_Register)]
public async Task<IActionResult> Register()
{
var ua = HttpContext.Request.Headers["User-Agent"][0] ?? "-";
var ip = _accessor.GetIpAddress();
// Legacy endpoint: generate a secret key for the user
var computedHash = StringUtils.Sha256String(StringUtils.GenerateRandomString(64) + DateTime.UtcNow.ToString());
var hashedKey = StringUtils.Sha256String(computedHash);
var dto = await _accountRegistrationService.RegisterAccountAsync(ua, ip, hashedKey);
return Json(new RegisterReplyDto()
{
Success = dto.Success,
ErrorMessage = dto.ErrorMessage,
UID = dto.UID,
SecretKey = computedHash
});
}
[AllowAnonymous]
[HttpPost(MareAuth.Auth_RegisterV2)]
public async Task<IActionResult> RegisterV2(string hashedSecretKey)
{
if (string.IsNullOrEmpty(hashedSecretKey)) return BadRequest("No HashedSecretKey");
if (hashedSecretKey.Length != 64) return BadRequest("Bad HashedSecretKey");
if (!hashedSecretKey.All(char.IsAsciiHexDigitUpper)) return BadRequest("Bad HashedSecretKey");
var ua = HttpContext.Request.Headers["User-Agent"][0] ?? "-";
var ip = _accessor.GetIpAddress();
return Json(await _accountRegistrationService.RegisterAccountAsync(ua, ip, hashedSecretKey));
}
private JwtSecurityToken CreateToken(IEnumerable<Claim> authClaims)
{
var authSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration.GetValue<string>(nameof(MareConfigurationBase.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

@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Content Remove="appsettings.Development.json" />
<Content Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<None Include="appsettings.Development.json" />
<None Include="appsettings.json">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="IDisposableAnalyzers" Version="4.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.149">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MaxMind.GeoIP2" Version="5.2.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MareSynchronosShared\MareSynchronosShared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,40 @@
namespace MareSynchronosAuthService;
public class Program
{
public static void Main(string[] args)
{
var hostBuilder = CreateHostBuilder(args);
using var host = hostBuilder.Build();
try
{
host.Run();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.ClearProviders();
builder.AddConsole();
});
var logger = loggerFactory.CreateLogger<Startup>();
return Host.CreateDefaultBuilder(args)
.UseSystemd()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseContentRoot(AppContext.BaseDirectory);
webBuilder.ConfigureLogging((ctx, builder) =>
{
builder.AddConfiguration(ctx.Configuration.GetSection("Logging"));
builder.AddFile(o => o.RootPath = AppContext.BaseDirectory);
});
webBuilder.UseStartup(ctx => new Startup(ctx.Configuration, logger));
});
}
}

View File

@@ -0,0 +1,29 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:37726",
"sslPort": 0
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5056",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,226 @@
using MareSynchronosAuthService.Controllers;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Services;
using MareSynchronosShared.Utils;
using Microsoft.AspNetCore.Mvc.Controllers;
using StackExchange.Redis.Extensions.Core.Configuration;
using StackExchange.Redis.Extensions.System.Text.Json;
using StackExchange.Redis;
using System.Net;
using MareSynchronosAuthService.Services;
using MareSynchronosShared.RequirementHandlers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using MareSynchronosShared.Data;
using Microsoft.EntityFrameworkCore;
using Prometheus;
using MareSynchronosShared.Utils.Configuration;
namespace MareSynchronosAuthService;
public class Startup
{
private readonly IConfiguration _configuration;
private ILogger<Startup> _logger;
public Startup(IConfiguration configuration, ILogger<Startup> logger)
{
_configuration = configuration;
_logger = logger;
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationBase>>();
app.UseRouting();
app.UseHttpMetrics();
app.UseAuthentication();
app.UseAuthorization();
KestrelMetricServer metricServer = new KestrelMetricServer(config.GetValueOrDefault<int>(nameof(MareConfigurationBase.MetricsPort), 4985));
metricServer.Start();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast<RouteEndpoint>())
{
if (source == null) continue;
_logger.LogInformation("Endpoint: {url} ", source.RoutePattern.RawText);
}
});
}
public void ConfigureServices(IServiceCollection services)
{
var mareConfig = _configuration.GetRequiredSection("MareSynchronos");
services.AddHttpContextAccessor();
ConfigureRedis(services, mareConfig);
services.AddSingleton<SecretKeyAuthenticatorService>();
services.AddSingleton<AccountRegistrationService>();
services.AddSingleton<GeoIPService>();
services.AddHostedService(provider => provider.GetRequiredService<GeoIPService>());
services.Configure<AuthServiceConfiguration>(_configuration.GetRequiredSection("MareSynchronos"));
services.Configure<MareConfigurationBase>(_configuration.GetRequiredSection("MareSynchronos"));
services.AddSingleton<ServerTokenGenerator>();
ConfigureAuthorization(services);
ConfigureDatabase(services, mareConfig);
ConfigureConfigServices(services);
ConfigureMetrics(services);
services.AddHealthChecks();
services.AddControllers().ConfigureApplicationPartManager(a =>
{
a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First());
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(JwtController)));
});
}
private static void ConfigureAuthorization(IServiceCollection services)
{
services.AddTransient<IAuthorizationHandler, UserRequirementHandler>();
services.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
.Configure<IConfigurationService<MareConfigurationBase>>((options, config) =>
{
options.TokenValidationParameters = new()
{
ValidateIssuer = false,
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue<string>(nameof(MareConfigurationBase.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(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build();
options.AddPolicy("Authenticated", policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
options.AddPolicy("Identified", policy =>
{
policy.AddRequirements(new UserRequirement(UserRequirements.Identified));
});
options.AddPolicy("Admin", policy =>
{
policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Administrator));
});
options.AddPolicy("Moderator", policy =>
{
policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator));
});
options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(MareClaimTypes.Internal, "true").Build());
});
}
private static void ConfigureMetrics(IServiceCollection services)
{
services.AddSingleton<MareMetrics>(m => new MareMetrics(m.GetService<ILogger<MareMetrics>>(), new List<string>
{
MetricsAPI.CounterAuthenticationCacheHits,
MetricsAPI.CounterAuthenticationFailures,
MetricsAPI.CounterAuthenticationRequests,
MetricsAPI.CounterAuthenticationSuccesses,
MetricsAPI.CounterAccountsCreated,
}, new List<string>
{
}));
}
private static void ConfigureRedis(IServiceCollection services, IConfigurationSection mareConfig)
{
// configure redis for SignalR
var redisConnection = mareConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty);
var options = ConfigurationOptions.Parse(redisConnection);
var endpoint = options.EndPoints[0];
string address = "";
int port = 0;
if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; }
if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; }
var redisConfiguration = new RedisConfiguration()
{
AbortOnConnectFail = true,
KeyPrefix = "",
Hosts = new RedisHost[]
{
new RedisHost(){ Host = address, Port = port },
},
AllowAdmin = true,
ConnectTimeout = options.ConnectTimeout,
Database = 0,
Ssl = false,
Password = options.Password,
ServerEnumerationStrategy = new ServerEnumerationStrategy()
{
Mode = ServerEnumerationStrategy.ModeOptions.All,
TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any,
UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw,
},
MaxValueLength = 1024,
PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50),
SyncTimeout = options.SyncTimeout,
};
services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration);
}
private void ConfigureConfigServices(IServiceCollection services)
{
services.AddSingleton<IConfigurationService<AuthServiceConfiguration>, MareConfigurationServiceServer<AuthServiceConfiguration>>();
services.AddSingleton<IConfigurationService<MareConfigurationBase>, MareConfigurationServiceServer<MareConfigurationBase>>();
}
private void ConfigureDatabase(IServiceCollection services, IConfigurationSection mareConfig)
{
services.AddDbContextPool<MareDbContext>(options =>
{
options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder =>
{
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
builder.MigrationsAssembly("MareSynchronosShared");
}).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false);
}, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024));
services.AddDbContextFactory<MareDbContext>(options =>
{
options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder =>
{
builder.MigrationsHistoryTable("_efmigrationshistory", "public");
builder.MigrationsAssembly("MareSynchronosShared");
}).UseSnakeCaseNamingConvention();
options.EnableThreadSafetyChecks(false);
});
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}