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 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 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(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 { { "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() != 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 { ["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 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()), 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)]; } }); } } }