Add remaining project files
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
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)];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user