Add Steam auth, platform validators & name gen
This commit is contained in:
@@ -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>();
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>(
|
||||
|
||||
89
RecNet.Infrastructure/Services/Steam/SteamAuthService.cs
Normal file
89
RecNet.Infrastructure/Services/Steam/SteamAuthService.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
48
RecNet.Infrastructure/Services/Steam/SteamAuthValidator.cs
Normal file
48
RecNet.Infrastructure/Services/Steam/SteamAuthValidator.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
8
RecNet.Infrastructure/Services/Steam/SteamOptions.cs
Normal file
8
RecNet.Infrastructure/Services/Steam/SteamOptions.cs
Normal 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";
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user