364 lines
14 KiB
C#
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)];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} |