Add Steam auth, platform validators & name gen

This commit is contained in:
Holden
2026-06-19 14:48:19 -05:00
parent 0b81cb046a
commit 63730dbba0
24 changed files with 592 additions and 17 deletions

View File

@@ -5,6 +5,7 @@ using RecNet.Domain.Repositories;
using RecNet.Infrastructure.Persistence;
using RecNet.Infrastructure.Persistence.Repositories;
using RecNet.Infrastructure.Services.Configuration;
using RecNet.Infrastructure.Services.Steam;
using RecNet.Infrastructure.Services.Tokens;
namespace RecNet.Infrastructure;
@@ -21,6 +22,17 @@ public static class DependencyInjection
.Bind(builder.Configuration.GetSection("Jwt"))
.Validate(options => !string.IsNullOrWhiteSpace(options.Secret), "JWT secret is required.")
.ValidateOnStart();
builder.Services
.AddOptions<SteamOptions>()
.Bind(builder.Configuration.GetSection("Steam"));
builder.Services.AddHttpClient<ISteamAuthService, SteamAuthService>(client =>
{
client.BaseAddress = new Uri("https://api.steampowered.com/");
});
builder.Services.AddScoped<IPlatformAuthValidator, SteamAuthValidator>();
builder.Services.AddScoped<IProfileRepository, ProfileRepository>();
builder.Services.AddScoped<IServerConfigRepository, ServerConfigRepository>();

View File

@@ -8,6 +8,8 @@
<ItemGroup>
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="13.4.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.8" />
<PackageReference Include="SteamApi.Models" Version="1.1.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
</ItemGroup>

View File

@@ -8,16 +8,25 @@ public class ConfigService(IServerConfigRepository repository) : IConfigService
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
public async Task<T?> GetAsync<T>(
public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
{
var config = await repository.GetAsync(key, ct);
if (config == null)
return default;
return JsonSerializer.Deserialize<T>(config.Value, JsonOptions);
}
public async Task<T> GetAsync<T>(
string key,
T? defaultValue = default,
T defaultValue,
CancellationToken ct = default)
{
var config = await repository.GetAsync(key, ct);
if (config == null)
return defaultValue;
return JsonSerializer.Deserialize<T>(config.Value, JsonOptions);
return JsonSerializer.Deserialize<T>(config.Value, JsonOptions) ?? defaultValue;
}
public async Task SetAsync<T>(

View File

@@ -0,0 +1,89 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
using RecNet.Application.Common.Interfaces;
using RecNet.Application.Common.Steam;
using SteamApi.Models.Steam.Player;
using SteamApi.Models.Steam.Responses;
namespace RecNet.Infrastructure.Services.Steam;
public class SteamAuthService(
HttpClient httpClient,
IOptions<SteamOptions> options) : ISteamAuthService
{
private readonly SteamOptions _options = options.Value;
public async Task<SteamAuthResult> AuthorizeAsync(
string ticket,
uint appId,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(ticket))
return SteamAuthResult.Failure();
if (appId != _options.AppId)
return SteamAuthResult.Failure();
var steamId = await AuthenticateTicketAsync(ticket, ct);
if (steamId is null)
return SteamAuthResult.Failure();
var profile = await GetProfileAsync(steamId, ct);
return SteamAuthResult.Success(
steamId,
profile?.PersonaName);
}
private async Task<string?> AuthenticateTicketAsync(
string ticket,
CancellationToken ct)
{
var url =
"ISteamUserAuth/AuthenticateUserTicket/v0001/" +
$"?key={Uri.EscapeDataString(_options.ApiKey)}" +
$"&appid={_options.AppId}" +
$"&ticket={Uri.EscapeDataString(ticket)}";
var response = await httpClient.GetAsync(url, ct);
if (!response.IsSuccessStatusCode)
return null;
var body = await response.Content.ReadFromJsonAsync<SteamResponse<SteamAuthResponse>>(
cancellationToken: ct);
return body?.Response?.Params?.SteamId;
}
private async Task<PlayerSummary?> GetProfileAsync(
string steamId,
CancellationToken ct)
{
var url =
"ISteamUser/GetPlayerSummaries/v0002/" +
$"?key={Uri.EscapeDataString(_options.ApiKey)}" +
$"&steamids={Uri.EscapeDataString(steamId)}";
var response = await httpClient.GetAsync(url, ct);
if (!response.IsSuccessStatusCode)
return null;
var body = await response.Content.ReadFromJsonAsync<SteamResponse<PlayerSummariesResponse>>(
cancellationToken: ct);
return body?.Response?.Players.FirstOrDefault();
}
private class SteamAuthResponse
{
[JsonPropertyName("params")]
public SteamAuthParamsResponse? Params { get; set; }
}
private class SteamAuthParamsResponse
{
[JsonPropertyName("steamid")]
public required string SteamId { get; set; }
}
}

View File

@@ -0,0 +1,48 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using RecNet.Application.Common.Interfaces;
using RecNet.Application.Common.PlatformAuth;
using RecNet.Domain.Common;
namespace RecNet.Infrastructure.Services.Steam;
public class SteamAuthValidator(ISteamAuthService steamAuthService) : IPlatformAuthValidator
{
public PlatformType PlatformType
=> PlatformType.Steamworks;
public async Task<PlatformAuthResult> ValidateAsync(
string platformAuthentication,
string platformId,
CancellationToken ct = default)
{
var steamParams = JsonSerializer.Deserialize<SteamPlatformAuth>(platformAuthentication);
if (steamParams == null)
return PlatformAuthResult.Failure("Invalid authentication");
var result = await steamAuthService.AuthorizeAsync(steamParams.Ticket, steamParams.AppId.AppId, ct);
if (!result.Succeeded || result.SteamId is null)
return PlatformAuthResult.Failure("Invalid authentication");
if (!string.IsNullOrWhiteSpace(platformId) &&
platformId != result.SteamId)
return PlatformAuthResult.Failure("Nice try");
return PlatformAuthResult.Success(result.DisplayName);
}
private sealed class SteamPlatformAuth
{
public required string Ticket { get; set; }
public required SteamAppIdAuth AppId { get; set; }
}
// Splooty, why did you set it up like this ;-;
private sealed class SteamAppIdAuth
{
[JsonPropertyName("m_AppId")]
public required uint AppId { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace RecNet.Infrastructure.Services.Steam;
public class SteamOptions
{
public string ApiKey { get; set; } = string.Empty;
public uint AppId { get; set; } = 480; // Spacewar
public string Identity { get; set; } = "recnet";
}

View File

@@ -30,7 +30,7 @@ public class TokenService(IOptions<JwtOptions> options) : ITokenService
};
if (profile.IsModerator)
claims.Add(new Claim(ClaimTypes.Role, "moderator"));
claims.Add(new Claim("role", "moderator"));
var token = new JwtSecurityToken(
issuer: jwtOptions.Issuer,