Use native game auth client only

This commit is contained in:
Alexa
2026-06-30 14:29:55 +03:00
parent 995c3a90dd
commit 8bb73dcdb1
4 changed files with 6 additions and 170 deletions
@@ -3,7 +3,6 @@ using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Text.Json;
using Duende.IdentityServer; using Duende.IdentityServer;
using Duende.IdentityServer.Models; using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services; using Duende.IdentityServer.Services;
@@ -130,9 +129,8 @@ public static class RecRoomIdentityServerRegistration
internal static class RecRoomIdentityServerConfig internal static class RecRoomIdentityServerConfig
{ {
public const string ClientId = "recroom.client"; public const string ClientId = "recroom";
public const string NativeClientId = "recroom"; public const string ClientSecret = "VxZ53kgbbEaRoZAeMe00MagtgD12GLL2";
public const string NativeClientSecret = "VxZ53kgbbEaRoZAeMe00MagtgD12GLL2";
public const string GameClientScope = "gameClient"; public const string GameClientScope = "gameClient";
public const string CachedLoginGrantType = "cached_login"; public const string CachedLoginGrantType = "cached_login";
@@ -159,25 +157,9 @@ internal static class RecRoomIdentityServerConfig
new() new()
{ {
ClientId = ClientId, ClientId = ClientId,
ClientName = "Rec Room compatibility client", ClientName = "Rec Room game client",
RequireClientSecret = false,
AllowedGrantTypes = { GrantType.ResourceOwnerPassword, GrantType.RefreshToken, CachedLoginGrantType },
AllowedScopes = RecRoomScopes(),
AllowOfflineAccess = true,
AccessTokenLifetime = 60 * 60 * 12,
AbsoluteRefreshTokenLifetime = 60 * 60 * 24 * 30,
SlidingRefreshTokenLifetime = 60 * 60 * 24 * 30,
RefreshTokenUsage = TokenUsage.ReUse,
RefreshTokenExpiration = TokenExpiration.Absolute,
UpdateAccessTokenClaimsOnRefresh = true,
AlwaysIncludeUserClaimsInIdToken = true,
},
new()
{
ClientId = NativeClientId,
ClientName = "Rec Room native client",
RequireClientSecret = true, RequireClientSecret = true,
ClientSecrets = { new Secret(NativeClientSecret.Sha256()) }, ClientSecrets = { new Secret(ClientSecret.Sha256()) },
AllowedGrantTypes = { GrantType.ResourceOwnerPassword, GrantType.RefreshToken, CachedLoginGrantType }, AllowedGrantTypes = { GrantType.ResourceOwnerPassword, GrantType.RefreshToken, CachedLoginGrantType },
AllowedScopes = RecRoomScopes(), AllowedScopes = RecRoomScopes(),
AllowOfflineAccess = true, AllowOfflineAccess = true,
@@ -201,7 +183,7 @@ internal static class RecRoomIdentityServerConfig
]; ];
} }
public sealed class IdentityServerLegacyTokenRequestMiddleware(RequestDelegate next) public sealed class IdentityServerGameTokenRequestMiddleware(RequestDelegate next)
{ {
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
{ {
@@ -218,12 +200,6 @@ public sealed class IdentityServerLegacyTokenRequestMiddleware(RequestDelegate n
StringComparer.OrdinalIgnoreCase); StringComparer.OrdinalIgnoreCase);
var changed = false; var changed = false;
if (!values.ContainsKey("client_id"))
{
values["client_id"] = RecRoomIdentityServerConfig.ClientId;
changed = true;
}
if (IsDevicePasswordGrant(values)) if (IsDevicePasswordGrant(values))
{ {
if (!values.ContainsKey("username") || string.IsNullOrWhiteSpace(values["username"].ToString())) if (!values.ContainsKey("username") || string.IsNullOrWhiteSpace(values["username"].ToString()))
@@ -291,114 +267,6 @@ public sealed class IdentityServerLegacyTokenRequestMiddleware(RequestDelegate n
} }
} }
public sealed class IdentityServerTokenResponseMiddleware(RequestDelegate next, DomainConfig domain)
{
public async Task InvokeAsync(HttpContext context)
{
if (!IsTokenRequest(context))
{
await next(context);
return;
}
var originalBody = context.Response.Body;
await using var buffer = new MemoryStream();
context.Response.Body = buffer;
await next(context);
context.Response.Body = originalBody;
buffer.Position = 0;
if (context.Response.StatusCode != StatusCodes.Status200OK ||
!IsJson(context.Response.ContentType))
{
await buffer.CopyToAsync(originalBody);
return;
}
using var document = await JsonDocument.ParseAsync(buffer);
var response = new Dictionary<string, object?>(StringComparer.Ordinal);
foreach (var property in document.RootElement.EnumerateObject())
response[property.Name] = ConvertJsonValue(property.Value);
if (response.TryGetValue("access_token", out var accessValue) &&
accessValue is string accessToken &&
!string.IsNullOrWhiteSpace(accessToken))
{
AddLegacyTokenAliases(response, accessToken);
SetAccessCookie(context, accessToken);
}
var bytes = JsonSerializer.SerializeToUtf8Bytes(response);
context.Response.ContentLength = bytes.Length;
context.Response.ContentType = "application/json; charset=utf-8";
await originalBody.WriteAsync(bytes);
}
private static bool IsTokenRequest(HttpContext context) =>
HttpMethods.IsPost(context.Request.Method)
&& context.Request.Path.Equals("/connect/token", StringComparison.OrdinalIgnoreCase);
private static bool IsJson(string? contentType) =>
contentType?.StartsWith("application/json", StringComparison.OrdinalIgnoreCase) == true;
private static object? ConvertJsonValue(JsonElement value) =>
value.ValueKind switch
{
JsonValueKind.String => value.GetString(),
JsonValueKind.Number when value.TryGetInt64(out var l) => l,
JsonValueKind.Number => value.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
_ => JsonSerializer.Deserialize<object>(value.GetRawText()),
};
private static void AddLegacyTokenAliases(Dictionary<string, object?> response, string accessToken)
{
response["Access_token"] = accessToken;
response["accessToken"] = accessToken;
response["AccessToken"] = accessToken;
response["token"] = accessToken;
response["Token"] = accessToken;
response["tokenType"] = "Bearer";
response["TokenType"] = "Bearer";
if (response.TryGetValue("expires_in", out var expiresIn))
{
response["expiresIn"] = expiresIn;
response["ExpiresIn"] = expiresIn;
}
if (response.TryGetValue("refresh_token", out var refreshToken))
{
response["Refresh_token"] = refreshToken;
response["refreshToken"] = refreshToken;
response["RefreshToken"] = refreshToken;
}
var signingKey = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
response["key"] = signingKey;
response["Key"] = signingKey;
}
private void SetAccessCookie(HttpContext context, string accessToken)
{
var cookie = new CookieOptions
{
HttpOnly = true,
Secure = string.Equals(domain.Scheme, "https", StringComparison.OrdinalIgnoreCase),
SameSite = SameSiteMode.None,
Expires = DateTimeOffset.UtcNow.AddHours(12),
IsEssential = true,
};
if (domain.Apex.Contains('.'))
cookie.Domain = "." + domain.Apex;
context.Response.Cookies.Append(AuthService.AccessCookieName, accessToken, cookie);
}
}
public sealed class RecRoomPasswordValidator( public sealed class RecRoomPasswordValidator(
PlayerService playerService, PlayerService playerService,
LevelService level, LevelService level,
@@ -1213,13 +1213,6 @@ public class AdminController(
Store = "in-memory", Store = "in-memory",
Warning = "Refresh tokens are process-local until an operational store is configured.", Warning = "Refresh tokens are process-local until an operational store is configured.",
}, },
LegacyCompatibility = new
{
InjectsClientId = true,
InjectsDefaultScopes = true,
AddsLegacyTokenAliases = true,
AcceptsLegacyJwtSigningKey = true,
},
}); });
} }
/// <summary>GET <c>api/admin/v1/settings</c> — current server-wide /// <summary>GET <c>api/admin/v1/settings</c> — current server-wide
@@ -62,8 +62,7 @@ public static class MiddlewarePipelineExtensions
app.UseMiddleware<IpBanCheckMiddleware>(); app.UseMiddleware<IpBanCheckMiddleware>();
app.UseMiddleware<IdentityServerTokenResponseMiddleware>(); app.UseMiddleware<IdentityServerGameTokenRequestMiddleware>();
app.UseMiddleware<IdentityServerLegacyTokenRequestMiddleware>();
app.UseIdentityServer(); app.UseIdentityServer();
app.UseAuthentication(); app.UseAuthentication();
@@ -36,12 +36,6 @@ interface IdentityServerSettings {
store: string; store: string;
warning: string; warning: string;
}; };
legacyCompatibility: {
injectsClientId: boolean;
injectsDefaultScopes: boolean;
addsLegacyTokenAliases: boolean;
acceptsLegacyJwtSigningKey: boolean;
};
} }
export function IdentityServer() { export function IdentityServer() {
@@ -126,15 +120,6 @@ export function IdentityServer() {
</div> </div>
</div> </div>
<div className="card !p-5">
<h2 className="text-sm font-semibold text-ink-50">Legacy compatibility</h2>
<div className="mt-4 grid gap-3 md:grid-cols-2">
<Compatibility label="Inject client_id for old clients" ok={settings.legacyCompatibility.injectsClientId} />
<Compatibility label="Merge default scopes" ok={settings.legacyCompatibility.injectsDefaultScopes} />
<Compatibility label="Add legacy token aliases" ok={settings.legacyCompatibility.addsLegacyTokenAliases} />
<Compatibility label="Accept legacy JWT signing key" ok={settings.legacyCompatibility.acceptsLegacyJwtSigningKey} />
</div>
</div>
</div> </div>
<div className="space-y-5"> <div className="space-y-5">
@@ -198,15 +183,6 @@ function Metric({ label, value }: { label: string; value: string }) {
); );
} }
function Compatibility({ label, ok }: { label: string; ok: boolean }) {
return (
<div className="flex min-h-10 items-center justify-between gap-3 rounded-md border border-ink-800 bg-ink-950/30 px-3 py-2">
<span className="text-xs text-ink-100">{label}</span>
<StatusBadge ok={ok} on="On" off="Off" />
</div>
);
}
function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) { function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return ( return (
<div> <div>