Files
VORTEX-REC-LEAK/Controllers/Auth/ConnectController.cs
T
2026-07-23 18:21:43 -07:00

364 lines
14 KiB
C#

using DeluxeBackend.Models;
using DeluxeBackend.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading.Tasks;
using static DeluxeBackend.Enums;
namespace DeluxeBackend.Controllers.Auth
{
public class GameTokenRequestDto
{
[FromForm(Name = "grant_type"), Required]
public required string GrantType { get; set; }
[FromForm(Name = "client_id"), Required]
public required string ClientId { get; set; }
[FromForm(Name = "client_secret"), Required]
public required string ClientSecret { get; set; }
[FromForm(Name = "platform"), Required]
public required PlatformType Platform { get; set; }
[FromForm(Name = "platform_id"), Required]
public required string PlatformId { get; set; }
[FromForm(Name = "device_id"), Required]
public required string DeviceId { get; set; }
[FromForm(Name = "device_class"), Required]
public required DeviceClassType DeviceClass { get; set; }
[FromForm(Name = "time"), Required]
public required DateTimeOffset Time { get; set; }
[FromForm(Name = "ver"), Required]
public required string Version { get; set; }
[FromForm(Name = "cid"), Required]
public required int Cid { get; set; }
[FromForm(Name = "build_key"), Required]
public required string BuildKey { get; set; }
[FromForm(Name = "asid"), Required]
public required long Asid { get; set; }
[FromForm(Name = "locale"), Required]
public required string Locale { get; set; }
[FromForm(Name = "isInitialLogin"), Required]
public required bool IsInitialLogin { get; set; }
[FromForm(Name = "dinfo")]
[MaxLength(2000)]
public string? DeviceInfo { get; set; }
[FromForm(Name = "eac_challenge")]
public string? EacChallenge { get; set; }
[FromForm(Name = "eac_response")]
public string? EacResponse { get; set; }
[FromForm(Name = "platform_auth"), Required]
public required string PlatformAuthRaw { get; set; }
[FromForm(Name = "account_id")]
public long? AccountId { get; set; } = null;
[FromForm(Name = "refresh_token")]
public string? RefreshToken { get; set; } = null;
[FromForm(Name = "username")]
[MaxLength(256)]
public string? Username { get; set; } = null;
[FromForm(Name = "password")]
[MaxLength(256)]
public string? Password { get; set; } = null;
}
public class OculusPlatformAuth
{
public required string Nonce { get; set; }
public required string AppId { get; set; }
public required string Source { get; set; }
}
[Route("Auth/connect")]
[ApiController]
public class ConnectController : ControllerBase
{
private readonly ILiteDbService db;
private readonly IJwtService jwt;
private readonly IPasswordService password;
private readonly HttpClient httpClient;
public static readonly ConcurrentDictionary<string, (string UserCode, long AccId, DateTime CreatedAt)> DeviceAuthorizationKeys = new();
public ConnectController(ILiteDbService _db, IJwtService _jwtService, IPasswordService _password, HttpClient _httpClient)
{
db = _db;
jwt = _jwtService;
password = _password;
httpClient = _httpClient;
}
[HttpPost("gametoken")]
[HttpPost("token")]
public async Task<IActionResult> GameToken([FromForm, Required] GameTokenRequestDto request)
{
if (request.ClientId != "recroom")
{
return BadRequest(new { error = "invalid_client" });
}
switch (request.Platform)
{
case PlatformType.Oculus:
try
{
var auth = JsonConvert.DeserializeObject<OculusPlatformAuth>(request.PlatformAuthRaw);
if (auth == null || string.IsNullOrWhiteSpace(auth.Nonce) || string.IsNullOrWhiteSpace(auth.AppId))
{
return BadRequest(new { error = "invalid_grant", error_description = "Meta authentication failed. Missing required nonces." });
}
if (!ServerConfig.OvrAppSecret.TryGetValue(auth.AppId, out var ovrAppSecret))
{
return BadRequest(new { error = "invalid_grant", error_description = "Oculus integration context not configured." });
}
var postData = new Dictionary<string, string>
{
{ "access_token", $"OC|{auth.AppId}|{ovrAppSecret}" },
{ "user_id", request.PlatformId },
{ "nonce", auth.Nonce }
};
var formContent = new FormUrlEncodedContent(postData);
var metaResponse = await httpClient.PostAsync("https://graph.oculus.com/user_nonce_validate", formContent);
if (!metaResponse.IsSuccessStatusCode)
{
return StatusCode(502, new { error = "invalid_grant", error_description = "Upstream identity tracking challenge rejected." });
}
var metaContent = await metaResponse.Content.ReadAsStringAsync();
var metaJson = JObject.Parse(metaContent);
if (metaJson["is_valid"]?.Value<bool>() != true)
{
return BadRequest(new { error = "invalid_grant", error_description = "Meta identity tracking validation signature verification failed." });
}
}
catch (JsonException)
{
return BadRequest(new { error = "invalid_grant", error_description = "JSONDecodeError" });
}
catch (Exception)
{
return StatusCode(500, new { error = "invalid_grant", error_description = "Internal Meta handling error." });
}
break;
default:
return BadRequest(new { error = "unsupported_platform_type" });
}
Account? account = null;
switch (request.GrantType)
{
case "create_account":
Account newacc = new() { Username = Guid.NewGuid().ToString("N") };
db.Accounts.Insert(newacc);
account = newacc;
Cachedlogin cachedlogin = new()
{
Platform = request.Platform,
PlatformId = request.PlatformId,
LastLoginAt = DateTime.UtcNow,
Account = newacc
};
db.Cachedlogins.Insert(cachedlogin);
break;
case "cached_login":
if (request.AccountId == null || request.AccountId <= 0)
{
return BadRequest(new { error = "invalid_request" });
}
var cached = db.Cachedlogins.Include(x => x.Account)
.FindOne(x => x.Platform == request.Platform && x.PlatformId == request.PlatformId && x.Account.Id == request.AccountId);
if (cached != null)
{
cached.LastLoginAt = DateTime.UtcNow;
db.Cachedlogins.Update(cached);
account = cached.Account;
}
break;
case "refresh_token":
if (string.IsNullOrWhiteSpace(request.RefreshToken))
{
return BadRequest(new { error = "invalid_request" });
}
RefreshToken? refreshToken = db.RefreshTokens.Include(x => x.Account).FindOne(x => x.Token == request.RefreshToken && !x.IsRevoked);
if (refreshToken == null || refreshToken.Expires < DateTime.UtcNow)
{
return BadRequest(new { error = "invalid_grant", error_description = "Token expired or blacklisted." });
}
refreshToken.IsRevoked = true;
db.RefreshTokens.Update(refreshToken);
account = refreshToken.Account;
break;
case "password":
if (string.IsNullOrWhiteSpace(request.Password) || string.IsNullOrWhiteSpace(request.Username))
{
return BadRequest(new { error = "invalid_request" });
}
Account? account1 = db.Accounts.FindOne(x => x.Username == request.Username);
if (account1 == null)
{
return BadRequest(new { error = "invalid_grant", error_description = "invalid_username_or_password" });
}
if (password.VerifyPassword(account1, request.Password) == Microsoft.AspNetCore.Identity.PasswordVerificationResult.Failed)
{
return BadRequest(new { error = "invalid_grant", error_description = "invalid_username_or_password" });
}
account = account1;
Cachedlogin cachedlogin1 = new()
{
Platform = request.Platform,
PlatformId = request.PlatformId,
LastLoginAt = DateTime.UtcNow,
Account = account1
};
db.Cachedlogins.Insert(cachedlogin1);
break;
default:
return BadRequest(new { error = "unsupported_grant_type" });
}
if (account == null)
{
return BadRequest(new { error = "invalid_grant", error_description = "Authentication reference lookups resolved to null." });
}
return Ok(new
{
access_token = await jwt.GenAccessToken(account, ["gameClient"], new Dictionary<string, object>
{
["db.platform"] = (int)request.Platform,
["db.platform.id"] = request.PlatformId,
["db.deviceclass"] = (int)request.DeviceClass,
["db.locale"] = request.Locale,
["db.appver"] = request.Version
}),
refresh_token = await jwt.GenRefreshToken(account),
key = ServerConfig.SigKey
});
}
[HttpPost("deviceauthorization")]
public IActionResult DeviceAuthorization()
{
CleanExpiredDeviceCodes();
string userCode = GenerateSecureCode(6).ToUpperInvariant();
string serverCode = GenerateSecureCode(30).ToUpperInvariant();
DeviceAuthorizationKeys[serverCode] = (userCode, -1, DateTime.UtcNow);
return Ok(new
{
device_code = serverCode,
user_code = userCode,
verification_uri = string.Empty,
verification_uri_complete = "calc://60+7",
expires_in = int.MaxValue,
interval = 5
});
}
[HttpPost("studiotoken")]
public async Task<IActionResult> StudioToken([FromForm(Name = "device_code")] string deviceCode)
{
if (string.IsNullOrWhiteSpace(deviceCode) || !DeviceAuthorizationKeys.TryGetValue(deviceCode, out var authData))
{
return NotFound(new { error = "invalid_grant", error_description = "The provided authorization tracker code was not found." });
}
if (DateTime.UtcNow - authData.CreatedAt > TimeSpan.FromMinutes(15))
{
DeviceAuthorizationKeys.TryRemove(deviceCode, out _);
return BadRequest(new { error = "code_expired" });
}
if (authData.AccId == -1)
{
return BadRequest(new { error = "authorization_pending" });
}
Account account = db.Accounts.FindById(authData.AccId);
if (account == null)
{
return BadRequest(new { error = "no_account" });
}
DeviceAuthorizationKeys.TryRemove(deviceCode, out _);
return Ok(new
{
access_token = await jwt.GenAccessToken(account, ["studioClient"], new Dictionary<string, object>()),
refresh_token = await jwt.GenRefreshToken(account),
scope = string.Empty,
expires_in = 3600
});
}
private static void CleanExpiredDeviceCodes()
{
var cutoff = DateTime.UtcNow.AddMinutes(-15);
foreach (var kvp in DeviceAuthorizationKeys)
{
if (kvp.Value.CreatedAt < cutoff)
{
DeviceAuthorizationKeys.TryRemove(kvp.Key, out _);
}
}
}
private static string GenerateSecureCode(int length)
{
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
return string.Create(length, chars, (buffer, symbols) =>
{
for (int i = 0; i < buffer.Length; i++)
{
buffer[buffer.Length - 1 - i] = symbols[RandomNumberGenerator.GetInt32(symbols.Length)];
}
});
}
}
}