Migrate server auth to IdentityServer

This commit is contained in:
Alexa
2026-06-30 13:27:54 +03:00
parent 955246c6e4
commit c0e7254aa1
15 changed files with 1156 additions and 300 deletions
+19 -33
View File
@@ -3,12 +3,16 @@ using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using DorkNet.Server.Auth;
using DorkNet.Server.Data;
namespace DorkNet.Server.Services;
public class AuthService(IConfiguration config, DorkNetDbContext db)
public class AuthService(IConfiguration config, DorkNetDbContext db, DomainConfig domain, IdentityServerSigningKeyProvider signingKeyProvider)
{
public const string AccessCookieName = "dorknet_access_token";
public const string LegacyIssuer = "https://api.rec.net/";
// Resolution order MUST match Program.cs's JwtBearer validator setup —
// env var first, config key second — otherwise tokens are signed with
// one secret and validated against another and every authenticated
@@ -38,38 +42,17 @@ public class AuthService(IConfiguration config, DorkNetDbContext db)
private string CreateJwt(long playerId, TimeSpan lifetime, string tokenType)
{
var handler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));
// Look up dev/admin flags so the JWT carries a "developer" role
// claim for accounts that should see the in-game debug button.
// The watch's Login.SetAccessToken parses the access_token's
// <c>role</c> claim — either a single string or a list — and
// calls <c>HasRole("developer", token)</c>; that result drives
// <c>Accounts.SetLocalAccountIsDev</c> and ultimately
// <c>SessionManager.IsDeveloper</c>, which the watch UI checks
// when deciding whether to show the dev console toggle.
var isDev = db.Players
.Where(p => p.Id == playerId)
.Select(p => (bool?)(p.IsDeveloper || p.IsAdmin))
.FirstOrDefault() ?? false;
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, playerId.ToString()),
new(ClaimTypes.Role, "gameClient"),
new("token_type", tokenType),
};
// Multiple Role claims become a JSON array on the wire — the
// watch's HasRole helper handles both string and List<string>
// forms.
if (isDev) claims.Add(new Claim(ClaimTypes.Role, "developer"));
var claims = RecRoomIdentityClaims.CreateAsync(db, playerId)
.GetAwaiter()
.GetResult();
claims.Add(new Claim("token_type", tokenType));
var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.Add(lifetime),
Issuer = "https://api.rec.net/",
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256),
Issuer = domain.AuthIssuer,
SigningCredentials = new SigningCredentials(signingKeyProvider.SigningKey, SecurityAlgorithms.RsaSha256),
};
return handler.WriteToken(handler.CreateJwtSecurityToken(descriptor));
@@ -80,19 +63,22 @@ public class AuthService(IConfiguration config, DorkNetDbContext db)
try
{
var handler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));
var principal = handler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
IssuerSigningKeys = signingKeyProvider.ValidationKeys,
ValidateIssuer = true,
ValidIssuer = "https://api.rec.net/",
ValidIssuers = new[] { domain.AuthIssuer, LegacyIssuer },
ValidateAudience = false,
ClockSkew = TimeSpan.Zero,
}, out _);
var idClaim = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var idClaim = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value
?? principal.FindFirst("sub")?.Value
?? principal.FindFirst("accountId")?.Value
?? principal.FindFirst("account_id")?.Value
?? principal.FindFirst("accountid")?.Value;
return idClaim is not null ? long.Parse(idClaim) : null;
}
catch
+1
View File
@@ -27,6 +27,7 @@ public sealed class DomainConfig
}
public string Sub(string prefix) => $"{prefix}.{Apex}";
public string AuthIssuer => SubUrl("auth");
public string Url(string host) => Port is null ? $"{Scheme}://{host}" : $"{Scheme}://{host}:{Port}";
public string SubUrl(string prefix) => Url(Sub(prefix));
public static bool MatchesSubdomain(string host, string prefix)
+14 -1
View File
@@ -681,8 +681,21 @@ public class PlayerService(DorkNetDbContext db, RoomService rooms, ILogger<Playe
// request `take=1000000` and exhaust the connection. 50 is enough
// for the watch's typeahead UI (it shows ~10 rows at a time).
var clamped = Math.Clamp(take, 1, 50);
var needle = (query ?? string.Empty).Trim().ToLower();
if (needle.Length == 0) return [];
var idQuery = long.TryParse(needle, out var parsedId) ? parsedId : -1;
return await db.Players
.Where(p => p.Username.Contains(query) || p.DisplayName.Contains(query))
.Where(p => p.Id == idQuery ||
p.Username.ToLower().Contains(needle) ||
p.DisplayName.ToLower().Contains(needle))
.OrderBy(p =>
p.Id == idQuery ||
p.Username.ToLower() == needle ||
p.DisplayName.ToLower() == needle ? 0 :
p.Username.ToLower().StartsWith(needle) ||
p.DisplayName.ToLower().StartsWith(needle) ? 1 : 2)
.ThenBy(p => p.Username)
.Take(clamped)
.ToListAsync();
}