mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-05-14 05:42:27 +00:00
Rework login and registration systems (#600)
* Initial work for verifying login ticket signatures * Add candidate psn public key * Add candidate psn public key and fix nuget packages * Finalize npticket changes * Add support for ticket version 3.0 * Rework login system to link platform accounts instead of using ip addresses * Make linked accounts green instead of blue * Fix api building * Fix unit tests * Actually fix unit tests * Set unit test user's linked platform * Why was this the wrong default value? * Fix username change code * Make TicketHash hash the entire ticket instead of just the serial * Send password setup email when user sets their email for the first time * Changes from self review
This commit is contained in:
parent
ff7969a147
commit
19ea44e0e2
37 changed files with 836 additions and 449 deletions
|
@ -3,7 +3,7 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "7.0.0",
|
||||
"version": "7.0.1",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
|
|
@ -71,8 +71,7 @@ public class UserEndpoints : ApiEndpointController
|
|||
[HttpPost("user/inviteToken/{username}")]
|
||||
public async Task<IActionResult> CreateUserInviteToken([FromRoute] string? username)
|
||||
{
|
||||
if (!Configuration.ServerConfiguration.Instance.Authentication.PrivateRegistration &&
|
||||
!Configuration.ServerConfiguration.Instance.Authentication.RegistrationEnabled)
|
||||
if (!Configuration.ServerConfiguration.Instance.Authentication.RegistrationEnabled)
|
||||
return this.NotFound();
|
||||
|
||||
string? authHeader = this.Request.Headers["Authorization"];
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#nullable enable
|
||||
using System.Net;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Extensions;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
using LBPUnion.ProjectLighthouse.Logging;
|
||||
using LBPUnion.ProjectLighthouse.Match.Rooms;
|
||||
|
@ -51,87 +52,138 @@ public class LoginController : ControllerBase
|
|||
if (remoteIpAddress == null)
|
||||
{
|
||||
Logger.Warn("unable to determine ip, rejecting login", LogArea.Login);
|
||||
return this.StatusCode(403, ""); // 403 probably isnt the best status code for this, but whatever
|
||||
return this.BadRequest();
|
||||
}
|
||||
|
||||
string ipAddress = remoteIpAddress.ToString();
|
||||
|
||||
string? username = npTicket.Username;
|
||||
|
||||
if (username == null)
|
||||
{
|
||||
Logger.Warn("Unable to determine username, rejecting login", LogArea.Login);
|
||||
return this.StatusCode(403, "");
|
||||
}
|
||||
|
||||
await this.database.RemoveExpiredTokens();
|
||||
|
||||
// Get an existing token from the IP & username
|
||||
GameToken? token = await this.database.GameTokens.Include(t => t.User)
|
||||
.FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == npTicket.Username && !t.Used);
|
||||
User? user;
|
||||
|
||||
if (token == null) // If we cant find an existing token, try to generate a new one
|
||||
switch (npTicket.Platform)
|
||||
{
|
||||
token = await this.database.AuthenticateUser(npTicket, ipAddress);
|
||||
case Platform.RPCS3:
|
||||
user = await this.database.Users.FirstOrDefaultAsync(u => u.LinkedRpcnId == npTicket.UserId);
|
||||
break;
|
||||
case Platform.PS3:
|
||||
case Platform.Vita:
|
||||
case Platform.UnitTest:
|
||||
user = await this.database.Users.FirstOrDefaultAsync(u => u.LinkedPsnId == npTicket.UserId);
|
||||
break;
|
||||
case Platform.PSP:
|
||||
case Platform.Unknown:
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
// If this user id hasn't been linked to any accounts
|
||||
if (user == null)
|
||||
{
|
||||
// Check if there is an account with that username already
|
||||
User? targetUsername = await this.database.Users.FirstOrDefaultAsync(u => u.Username == npTicket.Username);
|
||||
if (targetUsername != null)
|
||||
{
|
||||
ulong targetPlatform = npTicket.Platform == Platform.RPCS3
|
||||
? targetUsername.LinkedRpcnId
|
||||
: targetUsername.LinkedPsnId;
|
||||
|
||||
// only make a link request if the user doesn't already have an account linked for that platform
|
||||
if (targetPlatform != 0)
|
||||
{
|
||||
Logger.Warn($"New user tried to login but their name is already taken, username={username}", LogArea.Login);
|
||||
return this.StatusCode(403, "");
|
||||
}
|
||||
|
||||
// if there is already a pending link request don't create another
|
||||
bool linkAttemptExists = await this.database.PlatformLinkAttempts.AnyAsync(p =>
|
||||
p.Platform == npTicket.Platform &&
|
||||
p.PlatformId == npTicket.UserId &&
|
||||
p.UserId == targetUsername.UserId);
|
||||
|
||||
if (linkAttemptExists) return this.StatusCode(403, "");
|
||||
|
||||
PlatformLinkAttempt linkAttempt = new()
|
||||
{
|
||||
Platform = npTicket.Platform,
|
||||
UserId = targetUsername.UserId,
|
||||
IPAddress = ipAddress,
|
||||
Timestamp = TimeHelper.TimestampMillis,
|
||||
PlatformId = npTicket.UserId,
|
||||
};
|
||||
this.database.PlatformLinkAttempts.Add(linkAttempt);
|
||||
await this.database.SaveChangesAsync();
|
||||
Logger.Success($"User '{npTicket.Username}' tried to login but platform isn't linked, platform={npTicket.Platform}", LogArea.Login);
|
||||
return this.StatusCode(403, "");
|
||||
}
|
||||
|
||||
if (!ServerConfiguration.Instance.Authentication.AutomaticAccountCreation)
|
||||
{
|
||||
Logger.Warn($"Unknown user tried to connect username={username}", LogArea.Login);
|
||||
return this.StatusCode(403, "");
|
||||
}
|
||||
// create account for user if they don't exist
|
||||
user = await this.database.CreateUser(username, "$");
|
||||
user.Password = null;
|
||||
user.LinkedRpcnId = npTicket.Platform == Platform.RPCS3 ? npTicket.UserId : 0;
|
||||
user.LinkedPsnId = npTicket.Platform != Platform.RPCS3 ? npTicket.UserId : 0;
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
Logger.Success($"Created new user for {username}, platform={npTicket.Platform}", LogArea.Login);
|
||||
}
|
||||
// automatically change username if it doesn't match
|
||||
else if (user.Username != npTicket.Username)
|
||||
{
|
||||
bool usernameExists = await this.database.Users.AnyAsync(u => u.Username == npTicket.Username);
|
||||
if (usernameExists)
|
||||
{
|
||||
Logger.Warn($"{npTicket.Platform} user changed their name to a name that is already taken," +
|
||||
$" oldName='{user.Username}', newName='{npTicket.Username}'", LogArea.Login);
|
||||
return this.StatusCode(403, "");
|
||||
}
|
||||
Logger.Info($"User's username has changed, old='{user.Username}', new='{npTicket.Username}', platform={npTicket.Platform}", LogArea.Login);
|
||||
user.Username = username;
|
||||
this.database.PlatformLinkAttempts.RemoveWhere(p => p.UserId == user.UserId);
|
||||
// unlink other platforms because the names no longer match
|
||||
if (npTicket.Platform == Platform.RPCS3)
|
||||
user.LinkedPsnId = 0;
|
||||
else
|
||||
user.LinkedRpcnId = 0;
|
||||
|
||||
await this.database.SaveChangesAsync();
|
||||
}
|
||||
|
||||
GameToken? token = await this.database.GameTokens.Include(t => t.User)
|
||||
.FirstOrDefaultAsync(t => t.UserLocation == ipAddress && t.User.Username == npTicket.Username && t.TicketHash == npTicket.TicketHash);
|
||||
|
||||
if (token != null)
|
||||
{
|
||||
Logger.Warn($"Rejecting duplicate ticket from {username}", LogArea.Login);
|
||||
return this.StatusCode(403, "");
|
||||
}
|
||||
|
||||
token = await this.database.AuthenticateUser(user, npTicket, ipAddress);
|
||||
if (token == null)
|
||||
{
|
||||
Logger.Warn($"Unable to find/generate a token for username {npTicket.Username}", LogArea.Login);
|
||||
return this.StatusCode(403, ""); // If not, then 403.
|
||||
}
|
||||
}
|
||||
|
||||
// The GameToken LINQ statement above is case insensitive so we check that they are equal here
|
||||
if (token.User.Username != npTicket.Username)
|
||||
{
|
||||
Logger.Warn($"Username case does not match for user {npTicket.Username}, expected={token.User.Username}", LogArea.Login);
|
||||
return this.StatusCode(403, "");
|
||||
}
|
||||
|
||||
User? user = await this.database.UserFromGameToken(token);
|
||||
|
||||
if (user == null || user.IsBanned)
|
||||
if (user.IsBanned)
|
||||
{
|
||||
Logger.Error($"Unable to find user {npTicket.Username} from token", LogArea.Login);
|
||||
return this.StatusCode(403, "");
|
||||
}
|
||||
|
||||
if (ServerConfiguration.Instance.Mail.MailEnabled && (user.EmailAddress == null || !user.EmailAddressVerified))
|
||||
{
|
||||
Logger.Error($"Email address unverified for user {user.Username}", LogArea.Login);
|
||||
return this.StatusCode(403, "");
|
||||
}
|
||||
|
||||
if (ServerConfiguration.Instance.Authentication.UseExternalAuth)
|
||||
{
|
||||
if (user.ApprovedIPAddress == ipAddress)
|
||||
{
|
||||
token.Approved = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
AuthenticationAttempt authAttempt = new()
|
||||
{
|
||||
GameToken = token,
|
||||
GameTokenId = token.TokenId,
|
||||
Timestamp = TimeHelper.Timestamp,
|
||||
IPAddress = ipAddress,
|
||||
Platform = npTicket.Platform,
|
||||
};
|
||||
|
||||
this.database.AuthenticationAttempts.Add(authAttempt);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
token.Approved = true;
|
||||
}
|
||||
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
if (!token.Approved)
|
||||
{
|
||||
Logger.Warn($"Token unapproved for user {user.Username}, rejecting login", LogArea.Login);
|
||||
Logger.Error($"User {npTicket.Username} tried to login but is banned", LogArea.Login);
|
||||
return this.StatusCode(403, "");
|
||||
}
|
||||
|
||||
Logger.Success($"Successfully logged in user {user.Username} as {token.GameVersion} client", LogArea.Login);
|
||||
// After this point we are now considering this session as logged in.
|
||||
|
||||
// We just logged in with the token. Mark it as used so someone else doesnt try to use it,
|
||||
// and so we don't pick the same token up when logging in later.
|
||||
token.Used = true;
|
||||
|
||||
user.LastLogin = TimeHelper.TimestampMillis;
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
#nullable enable
|
||||
using System.Globalization;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Extensions;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
using LBPUnion.ProjectLighthouse.Logging;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers;
|
||||
|
||||
|
@ -58,11 +59,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.";
|
|||
#if DEBUG
|
||||
"\n\n---DEBUG INFO---\n" +
|
||||
$"user.UserId: {token.UserId}\n" +
|
||||
$"token.Approved: {token.Approved}\n" +
|
||||
$"token.Used: {token.Used}\n" +
|
||||
$"token.UserLocation: {token.UserLocation}\n" +
|
||||
$"token.GameVersion: {token.GameVersion}\n" +
|
||||
$"token.ExpiresAt: {token.ExpiresAt.ToString(CultureInfo.CurrentCulture)}\n" +
|
||||
$"token.TicketHash: {token.TicketHash}\n" +
|
||||
$"token.ExpiresAt: {token.ExpiresAt.ToString()}\n" +
|
||||
"---DEBUG INFO---" +
|
||||
#endif
|
||||
(string.IsNullOrWhiteSpace(announceText) ? "" : "\n")
|
||||
|
@ -81,13 +81,46 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.";
|
|||
{
|
||||
GameToken token = this.GetToken();
|
||||
|
||||
string response = await new StreamReader(this.Request.Body).ReadToEndAsync();
|
||||
string message = await new StreamReader(this.Request.Body).ReadToEndAsync();
|
||||
|
||||
string scannedText = CensorHelper.ScanMessage(response);
|
||||
if (message.StartsWith("/setemail "))
|
||||
{
|
||||
string email = message[(message.IndexOf(" ", StringComparison.Ordinal)+1)..];
|
||||
if (!SanitizationHelper.IsValidEmail(email)) return this.Ok();
|
||||
|
||||
if (await this.database.Users.AnyAsync(u => u.EmailAddress == email)) return this.Ok();
|
||||
|
||||
User? user = await this.database.UserFromGameToken(token);
|
||||
if (user == null || user.EmailAddress != null) return this.Ok();
|
||||
|
||||
PasswordResetToken resetToken = new()
|
||||
{
|
||||
Created = DateTime.Now,
|
||||
UserId = user.UserId,
|
||||
ResetToken = CryptoHelper.GenerateAuthToken(),
|
||||
};
|
||||
|
||||
string messageBody = $"Hello, {user.Username}.\n\n" +
|
||||
"A request to set your account's password was issued. If this wasn't you, this can probably be ignored.\n\n" +
|
||||
$"If this was you, your {ServerConfiguration.Instance.Customization.ServerName} password can be set at the following link:\n" +
|
||||
$"{ServerConfiguration.Instance.ExternalUrl}/passwordReset?token={resetToken.ResetToken}";
|
||||
|
||||
SMTPHelper.SendEmail(user.EmailAddress, $"Project Lighthouse Password Setup Request for {user.Username}", messageBody);
|
||||
|
||||
this.database.PasswordResetTokens.Add(resetToken);
|
||||
|
||||
user.EmailAddress = email;
|
||||
user.EmailAddressVerified = true;
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
return this.Ok();
|
||||
}
|
||||
|
||||
string scannedText = CensorHelper.ScanMessage(message);
|
||||
|
||||
string username = await this.database.UsernameFromGameToken(token);
|
||||
|
||||
Logger.Info($"{username}: {response} / {scannedText}", LogArea.Filter);
|
||||
Logger.Info($"{username}: {message} / {scannedText}", LogArea.Filter);
|
||||
|
||||
return this.Ok(scannedText);
|
||||
}
|
||||
|
|
|
@ -188,20 +188,14 @@ public class PhotosController : ControllerBase
|
|||
|
||||
List<int> photoSubjectIds = new();
|
||||
photoSubjectIds.AddRange(this.database.PhotoSubjects.Where(p => p.UserId == targetUserId).Select(p => p.PhotoSubjectId));
|
||||
List<Photo> photos = (from id in photoSubjectIds from p in
|
||||
this.database.Photos.Include(p => p.Creator).Where(p => p.PhotoSubjectCollection.Contains(id.ToString()))
|
||||
where p.PhotoSubjectCollection.Split(",").Contains(id.ToString()) && p.CreatorId != targetUserId select p).ToList();
|
||||
|
||||
var list = this.database.Photos.Select(p => new
|
||||
{
|
||||
p.PhotoId,
|
||||
p.PhotoSubjectCollection,
|
||||
}).ToList();
|
||||
List<int> photoIds = (from v in list where photoSubjectIds.Any(ps => v.PhotoSubjectCollection.Split(",").Contains(ps.ToString())) select v.PhotoId).ToList();
|
||||
|
||||
string response = Enumerable.Aggregate(
|
||||
this.database.Photos.Where(p => photoIds.Any(id => p.PhotoId == id) && p.CreatorId != targetUserId)
|
||||
string response = photos
|
||||
.OrderByDescending(s => s.Timestamp)
|
||||
.Skip(Math.Max(0, pageStart - 1))
|
||||
.Take(Math.Min(pageSize, 30)),
|
||||
string.Empty,
|
||||
.Take(Math.Min(pageSize, 30)).Aggregate(string.Empty,
|
||||
(current, photo) => current + photo.Serialize());
|
||||
|
||||
return this.Ok(LbpSerializer.StringElement("photos", response));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#nullable enable
|
||||
using LBPUnion.ProjectLighthouse.Extensions;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||
using LBPUnion.ProjectLighthouse.Types;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
@ -18,21 +18,54 @@ public class AuthenticationController : ControllerBase
|
|||
this.database = database;
|
||||
}
|
||||
|
||||
[HttpGet("unlink/{platform}")]
|
||||
public async Task<IActionResult> UnlinkPlatform(string platform)
|
||||
{
|
||||
User? user = this.database.UserFromWebRequest(this.Request);
|
||||
if (user == null) return this.Redirect("~/login");
|
||||
|
||||
Platform[] invalidTokens;
|
||||
|
||||
if (platform == "psn")
|
||||
{
|
||||
user.LinkedPsnId = 0;
|
||||
invalidTokens = new[] { Platform.PS3, Platform.Vita, };
|
||||
}
|
||||
else
|
||||
{
|
||||
user.LinkedRpcnId = 0;
|
||||
invalidTokens = new[] { Platform.RPCS3, };
|
||||
}
|
||||
|
||||
this.database.GameTokens.RemoveWhere(t => t.UserId == user.UserId && invalidTokens.Contains(t.Platform));
|
||||
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
return this.Redirect("~/authentication");
|
||||
}
|
||||
|
||||
[HttpGet("approve/{id:int}")]
|
||||
public async Task<IActionResult> Approve(int id)
|
||||
{
|
||||
User? user = this.database.UserFromWebRequest(this.Request);
|
||||
if (user == null) return this.Redirect("/login");
|
||||
|
||||
AuthenticationAttempt? authAttempt = await this.database.AuthenticationAttempts.Include
|
||||
(a => a.GameToken)
|
||||
.FirstOrDefaultAsync(a => a.AuthenticationAttemptId == id);
|
||||
if (authAttempt == null) return this.NotFound();
|
||||
PlatformLinkAttempt? linkAttempt = await this.database.PlatformLinkAttempts
|
||||
.FirstOrDefaultAsync(l => l.PlatformLinkAttemptId == id);
|
||||
if (linkAttempt == null) return this.NotFound();
|
||||
|
||||
if (authAttempt.GameToken.UserId != user.UserId) return this.StatusCode(403, "");
|
||||
if (linkAttempt.UserId != user.UserId) return this.NotFound();
|
||||
|
||||
authAttempt.GameToken.Approved = true;
|
||||
this.database.AuthenticationAttempts.Remove(authAttempt);
|
||||
if (linkAttempt.Platform == Platform.RPCS3)
|
||||
{
|
||||
user.LinkedRpcnId = linkAttempt.PlatformId;
|
||||
}
|
||||
else
|
||||
{
|
||||
user.LinkedPsnId = linkAttempt.PlatformId;
|
||||
}
|
||||
|
||||
this.database.PlatformLinkAttempts.Remove(linkAttempt);
|
||||
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
|
@ -45,37 +78,13 @@ public class AuthenticationController : ControllerBase
|
|||
User? user = this.database.UserFromWebRequest(this.Request);
|
||||
if (user == null) return this.Redirect("/login");
|
||||
|
||||
AuthenticationAttempt? authAttempt = await this.database.AuthenticationAttempts.Include
|
||||
(a => a.GameToken)
|
||||
.FirstOrDefaultAsync(a => a.AuthenticationAttemptId == id);
|
||||
if (authAttempt == null) return this.NotFound();
|
||||
PlatformLinkAttempt? linkAttempt = await this.database.PlatformLinkAttempts
|
||||
.FirstOrDefaultAsync(l => l.PlatformLinkAttemptId == id);
|
||||
if (linkAttempt == null) return this.NotFound();
|
||||
|
||||
if (authAttempt.GameToken.UserId != user.UserId) return this.StatusCode(403, "");
|
||||
if (linkAttempt.UserId != user.UserId) return this.NotFound();
|
||||
|
||||
this.database.GameTokens.Remove(authAttempt.GameToken);
|
||||
this.database.AuthenticationAttempts.Remove(authAttempt);
|
||||
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
return this.Redirect("~/authentication");
|
||||
}
|
||||
|
||||
[HttpGet("denyAll")]
|
||||
public async Task<IActionResult> DenyAll()
|
||||
{
|
||||
User? user = this.database.UserFromWebRequest(this.Request);
|
||||
if (user == null) return this.Redirect("/login");
|
||||
|
||||
List<AuthenticationAttempt> authAttempts = await this.database.AuthenticationAttempts.Include
|
||||
(a => a.GameToken)
|
||||
.Where(a => a.GameToken.UserId == user.UserId)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (AuthenticationAttempt authAttempt in authAttempts)
|
||||
{
|
||||
this.database.GameTokens.Remove(authAttempt.GameToken);
|
||||
this.database.AuthenticationAttempts.Remove(authAttempt);
|
||||
}
|
||||
this.database.PlatformLinkAttempts.Remove(linkAttempt);
|
||||
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
#nullable enable
|
||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||
using LBPUnion.ProjectLighthouse.Types;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Servers.Website.Controllers.ExternalAuth;
|
||||
|
||||
[ApiController]
|
||||
[Route("/authentication")]
|
||||
public class AutoApprovalController : ControllerBase
|
||||
{
|
||||
private readonly Database database;
|
||||
|
||||
public AutoApprovalController(Database database)
|
||||
{
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
[HttpGet("autoApprove/{id:int}")]
|
||||
public async Task<IActionResult> AutoApprove([FromRoute] int id)
|
||||
{
|
||||
User? user = this.database.UserFromWebRequest(this.Request);
|
||||
if (user == null) return this.Redirect("/login");
|
||||
|
||||
AuthenticationAttempt? authAttempt = await this.database.AuthenticationAttempts.Include
|
||||
(a => a.GameToken)
|
||||
.FirstOrDefaultAsync(a => a.AuthenticationAttemptId == id);
|
||||
|
||||
if (authAttempt == null) return this.BadRequest();
|
||||
if (authAttempt.GameToken.UserId != user.UserId) return this.Redirect("/login");
|
||||
|
||||
authAttempt.GameToken.Approved = true;
|
||||
user.ApprovedIPAddress = authAttempt.IPAddress;
|
||||
|
||||
this.database.AuthenticationAttempts.Remove(authAttempt);
|
||||
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
return this.Redirect("/authentication");
|
||||
}
|
||||
|
||||
[HttpGet("revokeAutoApproval")]
|
||||
public async Task<IActionResult> RevokeAutoApproval()
|
||||
{
|
||||
User? user = this.database.UserFromWebRequest(this.Request);
|
||||
if (user == null) return this.Redirect("/login");
|
||||
|
||||
user.ApprovedIPAddress = null;
|
||||
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
return this.Redirect("/authentication");
|
||||
}
|
||||
}
|
|
@ -4,62 +4,38 @@
|
|||
|
||||
@{
|
||||
Layout = "Layouts/BaseLayout";
|
||||
Model.Title = "Authentication";
|
||||
Model.Title = "Linked Accounts";
|
||||
string timeZone = Model.GetTimeZone();
|
||||
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
|
||||
}
|
||||
|
||||
@if (Model.AuthenticationAttempts.Count == 0)
|
||||
@if (Model.LinkAttempts.Count == 0)
|
||||
{
|
||||
<p>You have no pending authentication attempts.</p>
|
||||
<p>You have no pending link attempts.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>You have @Model.AuthenticationAttempts.Count authentication attempts pending.</p>
|
||||
<p>You have @Model.LinkAttempts.Count authentication attempts pending.</p>
|
||||
@if (Model.IpAddress != null)
|
||||
{
|
||||
<p>This device's IP address is <b>@(Model.IpAddress.ToString())</b>. If this matches with an authentication attempt below, then it's safe to assume the authentication attempt came from the same network as this device.</p>
|
||||
<p>This device's IP address is <b>@(Model.IpAddress.ToString())</b>. If this matches with a link attempt below, then it's safe to assume the link attempt came from the same network as this device.</p>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.User!.ApprovedIPAddress != null)
|
||||
@foreach (PlatformLinkAttempt authAttempt in Model.LinkAttempts)
|
||||
{
|
||||
<a href="/authentication/revokeAutoApproval">
|
||||
<button class="ui red button">
|
||||
<i class="trash icon"></i>
|
||||
<span>Revoke automatically approved IP Address (@Model.User!.ApprovedIPAddress)</span>
|
||||
</button>
|
||||
</a>
|
||||
}
|
||||
@if (Model.AuthenticationAttempts.Count > 1)
|
||||
{
|
||||
<a href="/authentication/denyAll">
|
||||
<button class="ui red button">
|
||||
<i class="x icon"></i>
|
||||
<span>Deny all</span>
|
||||
</button>
|
||||
</a>
|
||||
}
|
||||
|
||||
@foreach (AuthenticationAttempt authAttempt in Model.AuthenticationAttempts)
|
||||
{
|
||||
DateTimeOffset timestamp = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeSeconds(authAttempt.Timestamp), timeZoneInfo);
|
||||
DateTimeOffset timestamp = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(authAttempt.Timestamp), timeZoneInfo);
|
||||
<div class="ui red segment">
|
||||
<p>A <b>@authAttempt.Platform</b> authentication request was logged at <b>@timestamp.ToString("M/d/yyyy @ h:mm tt")</b> from the IP address <b>@authAttempt.IPAddress</b>.</p>
|
||||
<p>A <b>@authAttempt.Platform</b> link request was logged at <b>@timestamp.ToString("M/d/yyyy @ h:mm tt")</b> from the IP address <b>@authAttempt.IPAddress</b>.</p>
|
||||
<p><i class="yellow warning icon"></i> If you approve this request it will override any other linked accounts you have</p>
|
||||
<div>
|
||||
<a href="/authentication/autoApprove/@authAttempt.AuthenticationAttemptId">
|
||||
<a href="/authentication/approve/@authAttempt.PlatformLinkAttemptId">
|
||||
<button class="ui small green button">
|
||||
<i class="check icon"></i>
|
||||
<span>Automatically approve every time</span>
|
||||
<span>Approve</span>
|
||||
</button>
|
||||
</a>
|
||||
<a href="/authentication/approve/@authAttempt.AuthenticationAttemptId">
|
||||
<button class="ui small yellow button">
|
||||
<i class="check icon"></i>
|
||||
<span>Approve this time</span>
|
||||
</button>
|
||||
</a>
|
||||
<a href="/authentication/deny/@authAttempt.AuthenticationAttemptId">
|
||||
<a href="/authentication/deny/@authAttempt.PlatformLinkAttemptId">
|
||||
<button class="ui small red button">
|
||||
<i class="x icon"></i>
|
||||
<span>Deny</span>
|
||||
|
@ -68,3 +44,41 @@ else
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div style="display: inline-block">
|
||||
<h3 style="display: inline-block">PSN: </h3>
|
||||
@if (Model.User?.LinkedPsnId != 0)
|
||||
{
|
||||
<div class="ui green button" style="cursor: default; pointer-events: none">
|
||||
Linked
|
||||
</div>
|
||||
<a href="/authentication/unlink/psn" style="display: block; color: orangered">
|
||||
Click here to unlink this platform
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="ui button" style="cursor: default; pointer-events: none">
|
||||
Unlinked
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div>
|
||||
<h3 style="display: inline-block">RPCN: </h3>
|
||||
@if (Model.User?.LinkedRpcnId != 0)
|
||||
{
|
||||
<div class="ui green button" style="cursor: default; pointer-events: none">
|
||||
Linked
|
||||
</div>
|
||||
<a href="/authentication/unlink/rpcn" style="display: block; color: orangered">
|
||||
Click here to unlink this platform
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="ui button" style="cursor: default; pointer-events: none">
|
||||
Unlinked
|
||||
</button>
|
||||
}
|
||||
</div>
|
|
@ -1,18 +1,15 @@
|
|||
#nullable enable
|
||||
using System.Net;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
||||
using LBPUnion.ProjectLighthouse.Types;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.ExternalAuth;
|
||||
|
||||
public class AuthenticationPage : BaseLayout
|
||||
{
|
||||
|
||||
public List<AuthenticationAttempt> AuthenticationAttempts = new();
|
||||
public List<PlatformLinkAttempt> LinkAttempts = new();
|
||||
|
||||
public IPAddress? IpAddress;
|
||||
public AuthenticationPage(Database database) : base(database)
|
||||
|
@ -20,14 +17,12 @@ public class AuthenticationPage : BaseLayout
|
|||
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
if (!ServerConfiguration.Instance.Authentication.UseExternalAuth) return this.NotFound();
|
||||
if (this.User == null) return this.StatusCode(403, "");
|
||||
|
||||
this.IpAddress = this.HttpContext.Connection.RemoteIpAddress;
|
||||
|
||||
this.AuthenticationAttempts = this.Database.AuthenticationAttempts.Include
|
||||
(a => a.GameToken)
|
||||
.Where(a => a.GameToken.UserId == this.User.UserId)
|
||||
this.LinkAttempts = this.Database.PlatformLinkAttempts
|
||||
.Where(l => l.UserId == this.User.UserId)
|
||||
.OrderByDescending(a => a.Timestamp)
|
||||
.ToList();
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
@if (Model.User != null)
|
||||
{
|
||||
<p>@Model.Translate(LandingPageStrings.LoggedInAs, Model.User.Username)</p>
|
||||
if (ServerConfiguration.Instance.Authentication.UseExternalAuth && Model.PendingAuthAttempts > 0)
|
||||
if (Model.PendingAuthAttempts > 0)
|
||||
{
|
||||
<p>
|
||||
<b><a href="/authentication">@Model.Translate(LandingPageStrings.AuthAttemptsPending, Model.PendingAuthAttempts)</a></b>
|
||||
|
@ -30,18 +30,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
@if (Model.PlayersOnline.Count == 1)
|
||||
{
|
||||
<p>@Model.Translate(LandingPageStrings.UsersSingle)</p>
|
||||
}
|
||||
|
||||
else if (Model.PlayersOnline.Count == 0)
|
||||
@switch (Model.PlayersOnline.Count)
|
||||
{
|
||||
case 0:
|
||||
<p>@Model.Translate(LandingPageStrings.UsersNone)</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
case 1:
|
||||
<p>@Model.Translate(LandingPageStrings.UsersSingle)</p>
|
||||
break;
|
||||
default:
|
||||
<p>@Model.Translate(LandingPageStrings.UsersMultiple, Model.PlayersOnline.Count)</p>
|
||||
break;
|
||||
}
|
||||
|
||||
@{
|
||||
|
@ -49,7 +49,12 @@ else
|
|||
foreach (User user in Model.PlayersOnline)
|
||||
{
|
||||
i++;
|
||||
@await user.ToLink(Html, ViewData, language, timeZone, true)if (i != Model.PlayersOnline.Count){<span>,</span>} @* whitespace has forced my hand *@
|
||||
@await user.ToLink(Html, ViewData, language, timeZone, true)
|
||||
@* whitespace has forced my hand *@
|
||||
if (i != Model.PlayersOnline.Count)
|
||||
{
|
||||
<span>,</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,9 +27,8 @@ public class LandingPage : BaseLayout
|
|||
if (user != null && user.PasswordResetRequired) return this.Redirect("~/passwordResetRequired");
|
||||
|
||||
if (user != null)
|
||||
this.PendingAuthAttempts = await this.Database.AuthenticationAttempts.Include
|
||||
(a => a.GameToken)
|
||||
.CountAsync(a => a.GameToken.UserId == user.UserId);
|
||||
this.PendingAuthAttempts = await this.Database.PlatformLinkAttempts
|
||||
.CountAsync(l => l.UserId == user.UserId);
|
||||
|
||||
List<int> userIds = await this.Database.LastContacts.Where(l => TimeHelper.Timestamp - l.Timestamp < 300).Select(l => l.UserId).ToListAsync();
|
||||
|
||||
|
|
|
@ -11,11 +11,8 @@
|
|||
Model.NavigationItemsRight.Add(new PageNavigationItem(BaseLayoutStrings.HeaderLoginRegister, "/login", "sign in"));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ServerConfiguration.Instance.Authentication.UseExternalAuth)
|
||||
{
|
||||
Model.NavigationItems.Add(new PageNavigationItem(BaseLayoutStrings.HeaderAuthentication, "/authentication", "key"));
|
||||
}
|
||||
|
||||
@if (Model.User.IsAdmin)
|
||||
{
|
||||
|
|
|
@ -60,7 +60,7 @@ public class LoginForm : BaseLayout
|
|||
}
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
if (user == null || user.Password == null)
|
||||
{
|
||||
Logger.Warn($"User {username} failed to login on web due to invalid username", LogArea.Login);
|
||||
this.Error = ServerConfiguration.Instance.Mail.MailEnabled
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using JetBrains.Annotations;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
|
@ -36,7 +35,7 @@ public class PasswordResetRequestForm : BaseLayout
|
|||
return this.Page();
|
||||
}
|
||||
|
||||
if (!new EmailAddressAttribute().IsValid(email))
|
||||
if (!SanitizationHelper.IsValidEmail(email))
|
||||
{
|
||||
this.Error = "This email is in an invalid format";
|
||||
return this.Page();
|
||||
|
|
|
@ -25,30 +25,9 @@ public class RegisterForm : BaseLayout
|
|||
[SuppressMessage("ReSharper", "SpecifyStringComparison")]
|
||||
public async Task<IActionResult> OnPost(string username, string password, string confirmPassword, string emailAddress)
|
||||
{
|
||||
if (ServerConfiguration.Instance.Authentication.PrivateRegistration)
|
||||
{
|
||||
if (this.Request.Query.ContainsKey("token"))
|
||||
{
|
||||
string? token = this.Request.Query["token"];
|
||||
if (!this.Database.IsRegistrationTokenValid(token))
|
||||
return this.StatusCode(403, this.Translate(ErrorStrings.TokenInvalid));
|
||||
if (this.Database.UserFromWebRequest(this.Request) != null) return this.Redirect("~/");
|
||||
|
||||
string? tokenUsername = await this.Database.RegistrationTokens.Where(r => r.Token == token)
|
||||
.Select(u => u.Username)
|
||||
.FirstOrDefaultAsync();
|
||||
if (tokenUsername == null) return this.BadRequest();
|
||||
|
||||
username = tokenUsername;
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.NotFound();
|
||||
}
|
||||
}
|
||||
else if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled)
|
||||
{
|
||||
return this.NotFound();
|
||||
}
|
||||
if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled) return this.NotFound();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
|
@ -74,7 +53,8 @@ public class RegisterForm : BaseLayout
|
|||
return this.Page();
|
||||
}
|
||||
|
||||
if (await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower()) != null)
|
||||
User? existingUser = await this.Database.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower());
|
||||
if (existingUser != null)
|
||||
{
|
||||
this.Error = this.Translate(ErrorStrings.UsernameTaken);
|
||||
return this.Page();
|
||||
|
@ -93,11 +73,6 @@ public class RegisterForm : BaseLayout
|
|||
return this.Page();
|
||||
}
|
||||
|
||||
if (this.Request.Query.ContainsKey("token"))
|
||||
{
|
||||
await this.Database.RemoveRegistrationToken(this.Request.Query["token"]);
|
||||
}
|
||||
|
||||
User user = await this.Database.CreateUser(username, CryptoHelper.BCryptHash(password), emailAddress);
|
||||
|
||||
WebToken webToken = new()
|
||||
|
@ -112,35 +87,17 @@ public class RegisterForm : BaseLayout
|
|||
|
||||
this.Response.Cookies.Append("LighthouseToken", webToken.UserToken);
|
||||
|
||||
if (ServerConfiguration.Instance.Mail.MailEnabled) return this.Redirect("~/login/sendVerificationEmail");
|
||||
|
||||
return this.Redirect("~/");
|
||||
return ServerConfiguration.Instance.Mail.MailEnabled ?
|
||||
this.Redirect("~/login/sendVerificationEmail") :
|
||||
this.Redirect("~/");
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[SuppressMessage("ReSharper", "SpecifyStringComparison")]
|
||||
public async Task<IActionResult> OnGet()
|
||||
public IActionResult OnGet()
|
||||
{
|
||||
this.Error = string.Empty;
|
||||
if (ServerConfiguration.Instance.Authentication.PrivateRegistration)
|
||||
{
|
||||
if (this.Request.Query.ContainsKey("token"))
|
||||
{
|
||||
string? token = this.Request.Query["token"];
|
||||
if (!this.Database.IsRegistrationTokenValid(token))
|
||||
return this.StatusCode(403, this.Translate(ErrorStrings.TokenInvalid));
|
||||
|
||||
string? tokenUsername = await this.Database.RegistrationTokens.Where(r => r.Token == token)
|
||||
.Select(u => u.Username)
|
||||
.FirstAsync();
|
||||
this.Username = tokenUsername;
|
||||
}
|
||||
else
|
||||
{
|
||||
return this.NotFound();
|
||||
}
|
||||
}
|
||||
else if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled)
|
||||
if (!ServerConfiguration.Instance.Authentication.RegistrationEnabled)
|
||||
{
|
||||
return this.NotFound();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Files;
|
||||
|
@ -19,8 +18,6 @@ public class UserSettingsPage : BaseLayout
|
|||
public UserSettingsPage(Database database) : base(database)
|
||||
{}
|
||||
|
||||
private static bool IsValidEmail(string? email) => !string.IsNullOrWhiteSpace(email) && new EmailAddressAttribute().IsValid(email);
|
||||
|
||||
[SuppressMessage("ReSharper", "SpecifyStringComparison")]
|
||||
public async Task<IActionResult> OnPost([FromRoute] int userId, [FromForm] string? avatar, [FromForm] string? username, [FromForm] string? email, [FromForm] string? biography, [FromForm] string? timeZone, [FromForm] string? language)
|
||||
{
|
||||
|
@ -39,7 +36,9 @@ public class UserSettingsPage : BaseLayout
|
|||
|
||||
if (this.ProfileUser.Biography != biography && biography.Length <= 512) this.ProfileUser.Biography = biography;
|
||||
|
||||
if (ServerConfiguration.Instance.Mail.MailEnabled && IsValidEmail(email) && (this.User == this.ProfileUser || this.User.IsAdmin))
|
||||
if (ServerConfiguration.Instance.Mail.MailEnabled &&
|
||||
SanitizationHelper.IsValidEmail(email) &&
|
||||
(this.User == this.ProfileUser || this.User.IsAdmin))
|
||||
{
|
||||
// if email hasn't already been used
|
||||
if (!await this.Database.Users.AnyAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == email!.ToLower()))
|
||||
|
|
|
@ -35,7 +35,7 @@ public class MatchTests : LighthouseServerTest<GameServerTestStartup>
|
|||
await semaphore.WaitAsync();
|
||||
|
||||
HttpResponseMessage result = await this.AuthenticatedUploadDataRequest
|
||||
("LITTLEBIGPLANETPS3_XML/match", Encoding.ASCII.GetBytes("[UpdateMyPlayerData,[\"Player\":\"1984\"]]"), loginResult.AuthTicket);
|
||||
("LITTLEBIGPLANETPS3_XML/match", "[UpdateMyPlayerData,[\"Player\":\"1984\"]]"u8.ToArray(), loginResult.AuthTicket);
|
||||
|
||||
semaphore.Release();
|
||||
Assert.True(result.IsSuccessStatusCode);
|
||||
|
@ -53,7 +53,7 @@ public class MatchTests : LighthouseServerTest<GameServerTestStartup>
|
|||
int oldPlayerCount = await StatisticsHelper.RecentMatches(database);
|
||||
|
||||
HttpResponseMessage result = await this.AuthenticatedUploadDataRequest
|
||||
("LITTLEBIGPLANETPS3_XML/match", Encoding.ASCII.GetBytes("[UpdateMyPlayerData,[\"Player\":\"1984\"]]"), loginResult.AuthTicket);
|
||||
("LITTLEBIGPLANETPS3_XML/match", "[UpdateMyPlayerData,[\"Player\":\"1984\"]]"u8.ToArray(), loginResult.AuthTicket);
|
||||
|
||||
Assert.True(result.IsSuccessStatusCode);
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ public class RegisterTests : LighthouseWebTest
|
|||
{
|
||||
await using Database database = new();
|
||||
|
||||
string username = ("unitTestUser" + new Random().Next()).Substring(0, 16);
|
||||
string username = ("unitTestUser" + new Random().Next())[..16];
|
||||
string password = CryptoHelper.Sha256Hash(CryptoHelper.GenerateRandomBytes(64).ToArray());
|
||||
|
||||
await database.CreateUser(username, CryptoHelper.BCryptHash(password));
|
||||
|
|
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||
using System.Xml.Serialization;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||
using LBPUnion.ProjectLighthouse.Serialization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
|
@ -33,7 +34,11 @@ public class LighthouseServerTest<TStartup> where TStartup : class
|
|||
{
|
||||
await using Database database = new();
|
||||
if (await database.Users.FirstOrDefaultAsync(u => u.Username == $"{username}{number}") == null)
|
||||
await database.CreateUser($"{username}{number}", CryptoHelper.BCryptHash($"unitTestPassword{number}"));
|
||||
{
|
||||
User user = await database.CreateUser($"{username}{number}", CryptoHelper.BCryptHash($"unitTestPassword{number}"));
|
||||
user.LinkedPsnId = (ulong)number;
|
||||
await database.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: generate actual tickets
|
||||
|
@ -44,7 +49,7 @@ public class LighthouseServerTest<TStartup> where TStartup : class
|
|||
return response;
|
||||
}
|
||||
|
||||
public async Task<LoginResult> Authenticate(int number = 0)
|
||||
public async Task<LoginResult> Authenticate(int number = -1)
|
||||
{
|
||||
HttpResponseMessage response = await this.AuthenticateResponse(number);
|
||||
|
||||
|
|
|
@ -149,6 +149,7 @@
|
|||
<s:Boolean x:Key="/Default/UserDictionary/Words/=PCSA/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=PCSD/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=PCSF/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Rpcn/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Sublevels/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Swingy/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=thumbsdown/@EntryIndexedValue">True</s:Boolean>
|
||||
|
|
|
@ -5,6 +5,6 @@ namespace LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories;
|
|||
public class AuthenticationConfiguration
|
||||
{
|
||||
public bool RegistrationEnabled { get; set; } = true;
|
||||
public bool PrivateRegistration { get; set; } = false;
|
||||
public bool UseExternalAuth { get; set; }
|
||||
public bool AutomaticAccountCreation { get; set; } = true;
|
||||
public bool VerifyTickets { get; set; } = true;
|
||||
}
|
|
@ -153,7 +153,6 @@ internal class LegacyServerSettings
|
|||
configuration.Authentication = new AuthenticationConfiguration
|
||||
{
|
||||
RegistrationEnabled = this.RegistrationEnabled,
|
||||
UseExternalAuth = this.UseExternalAuth,
|
||||
};
|
||||
|
||||
configuration.Captcha = new CaptchaConfiguration
|
||||
|
|
|
@ -23,7 +23,7 @@ public class ServerConfiguration
|
|||
// You can use an ObsoleteAttribute instead. Make sure you set it to error, though.
|
||||
//
|
||||
// Thanks for listening~
|
||||
public const int CurrentConfigVersion = 14;
|
||||
public const int CurrentConfigVersion = 15;
|
||||
|
||||
#region Meta
|
||||
|
||||
|
|
|
@ -40,7 +40,6 @@ public class Database : DbContext
|
|||
public DbSet<LastContact> LastContacts { get; set; }
|
||||
public DbSet<VisitedLevel> VisitedLevels { get; set; }
|
||||
public DbSet<RatedLevel> RatedLevels { get; set; }
|
||||
public DbSet<AuthenticationAttempt> AuthenticationAttempts { get; set; }
|
||||
public DbSet<Review> Reviews { get; set; }
|
||||
public DbSet<RatedReview> RatedReviews { get; set; }
|
||||
public DbSet<DatabaseCategory> CustomCategories { get; set; }
|
||||
|
@ -53,6 +52,7 @@ public class Database : DbContext
|
|||
public DbSet<RegistrationToken> RegistrationTokens { get; set; }
|
||||
public DbSet<APIKey> APIKeys { get; set; }
|
||||
public DbSet<Playlist> Playlists { get; set; }
|
||||
public DbSet<PlatformLinkAttempt> PlatformLinkAttempts { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||
=> options.UseMySql(ServerConfiguration.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion);
|
||||
|
@ -102,9 +102,8 @@ public class Database : DbContext
|
|||
return user;
|
||||
}
|
||||
|
||||
public async Task<GameToken?> AuthenticateUser(NPTicket npTicket, string userLocation)
|
||||
public async Task<GameToken?> AuthenticateUser(User? user, NPTicket npTicket, string userLocation)
|
||||
{
|
||||
User? user = await this.Users.FirstOrDefaultAsync(u => u.Username == npTicket.Username);
|
||||
if (user == null) return null;
|
||||
|
||||
GameToken gameToken = new()
|
||||
|
@ -115,6 +114,7 @@ public class Database : DbContext
|
|||
UserLocation = userLocation,
|
||||
GameVersion = npTicket.GameVersion,
|
||||
Platform = npTicket.Platform,
|
||||
TicketHash = npTicket.TicketHash,
|
||||
// we can get away with a low expiry here since LBP will just get a new token everytime it gets 403'd
|
||||
ExpiresAt = DateTime.Now + TimeSpan.FromHours(1),
|
||||
};
|
||||
|
@ -341,13 +341,11 @@ public class Database : DbContext
|
|||
return await this.Users.FirstOrDefaultAsync(u => u.UserId == token.UserId);
|
||||
}
|
||||
|
||||
private async Task<User?> UserFromMMAuth(string authToken, bool allowUnapproved = false)
|
||||
private async Task<User?> UserFromMMAuth(string authToken)
|
||||
{
|
||||
if (ServerStatics.IsUnitTesting) allowUnapproved = true;
|
||||
GameToken? token = await this.GameTokens.FirstOrDefaultAsync(t => t.UserToken == authToken);
|
||||
|
||||
if (token == null) return null;
|
||||
if (!allowUnapproved && !token.Approved) return null;
|
||||
|
||||
if (DateTime.Now <= token.ExpiresAt) return await this.Users.FirstOrDefaultAsync(u => u.UserId == token.UserId);
|
||||
|
||||
|
@ -357,23 +355,20 @@ public class Database : DbContext
|
|||
return null;
|
||||
}
|
||||
|
||||
public async Task<User?> UserFromGameRequest(HttpRequest request, bool allowUnapproved = false)
|
||||
public async Task<User?> UserFromGameRequest(HttpRequest request)
|
||||
{
|
||||
if (ServerStatics.IsUnitTesting) allowUnapproved = true;
|
||||
if (!request.Cookies.TryGetValue("MM_AUTH", out string? mmAuth) || mmAuth == null) return null;
|
||||
if (!request.Cookies.TryGetValue("MM_AUTH", out string? mmAuth)) return null;
|
||||
|
||||
return await this.UserFromMMAuth(mmAuth, allowUnapproved);
|
||||
return await this.UserFromMMAuth(mmAuth);
|
||||
}
|
||||
|
||||
public async Task<GameToken?> GameTokenFromRequest(HttpRequest request, bool allowUnapproved = false)
|
||||
public async Task<GameToken?> GameTokenFromRequest(HttpRequest request)
|
||||
{
|
||||
if (ServerStatics.IsUnitTesting) allowUnapproved = true;
|
||||
if (!request.Cookies.TryGetValue("MM_AUTH", out string? mmAuth) || mmAuth == null) return null;
|
||||
if (!request.Cookies.TryGetValue("MM_AUTH", out string? mmAuth)) return null;
|
||||
|
||||
GameToken? token = await this.GameTokens.FirstOrDefaultAsync(t => t.UserToken == mmAuth);
|
||||
|
||||
if (token == null) return null;
|
||||
if (!allowUnapproved && !token.Approved) return null;
|
||||
|
||||
if (DateTime.Now <= token.ExpiresAt) return token;
|
||||
|
||||
|
@ -383,14 +378,12 @@ public class Database : DbContext
|
|||
return null;
|
||||
}
|
||||
|
||||
public async Task<(User, GameToken)?> UserAndGameTokenFromRequest(HttpRequest request, bool allowUnapproved = false)
|
||||
public async Task<(User, GameToken)?> UserAndGameTokenFromRequest(HttpRequest request)
|
||||
{
|
||||
if (ServerStatics.IsUnitTesting) allowUnapproved = true;
|
||||
if (!request.Cookies.TryGetValue("MM_AUTH", out string? mmAuth) || mmAuth == null) return null;
|
||||
if (!request.Cookies.TryGetValue("MM_AUTH", out string? mmAuth)) return null;
|
||||
|
||||
GameToken? token = await this.GameTokens.FirstOrDefaultAsync(t => t.UserToken == mmAuth);
|
||||
if (token == null) return null;
|
||||
if (!allowUnapproved && !token.Approved) return null;
|
||||
|
||||
if (DateTime.Now > token.ExpiresAt)
|
||||
{
|
||||
|
@ -527,7 +520,6 @@ public class Database : DbContext
|
|||
|
||||
foreach (Slot slot in this.Slots.Where(s => s.CreatorId == user.UserId)) await this.RemoveSlot(slot, false);
|
||||
|
||||
this.AuthenticationAttempts.RemoveRange(this.AuthenticationAttempts.Include(a => a.GameToken).Where(a => a.GameToken.UserId == user.UserId));
|
||||
this.HeartedProfiles.RemoveRange(this.HeartedProfiles.Where(h => h.UserId == user.UserId));
|
||||
this.PhotoSubjects.RemoveRange(this.PhotoSubjects.Where(s => s.UserId == user.UserId));
|
||||
this.HeartedLevels.RemoveRange(this.HeartedLevels.Where(h => h.UserId == user.UserId));
|
||||
|
|
|
@ -170,7 +170,7 @@ public static class CryptoHelper
|
|||
|
||||
public static string Sha1Hash(string str) => Sha1Hash(Encoding.UTF8.GetBytes(str));
|
||||
|
||||
public static string Sha1Hash(byte[] bytes) => BitConverter.ToString(SHA1.Create().ComputeHash(bytes)).Replace("-", "");
|
||||
public static string Sha1Hash(byte[] bytes) => BitConverter.ToString(SHA1.HashData(bytes)).Replace("-", "");
|
||||
|
||||
public static string BCryptHash(string str) => BCrypt.Net.BCrypt.HashPassword(str);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
|
@ -15,6 +16,10 @@ public static class SanitizationHelper
|
|||
{"'", "'"},
|
||||
};
|
||||
|
||||
public static bool IsValidEmail
|
||||
(string? email) =>
|
||||
!string.IsNullOrWhiteSpace(email) && new EmailAddressAttribute().IsValid(email);
|
||||
|
||||
public static void SanitizeStringsInClass(object? instance)
|
||||
{
|
||||
if (instance == null) return;
|
||||
|
|
118
ProjectLighthouse/Migrations/20221217002014_ReworkGameTokens.cs
Normal file
118
ProjectLighthouse/Migrations/20221217002014_ReworkGameTokens.cs
Normal file
|
@ -0,0 +1,118 @@
|
|||
using LBPUnion.ProjectLighthouse;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ProjectLighthouse.Migrations
|
||||
{
|
||||
[DbContext(typeof(Database))]
|
||||
[Migration("20221217002014_ReworkGameTokens")]
|
||||
public partial class ReworkGameTokens : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AuthenticationAttempts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ApprovedIPAddress",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Approved",
|
||||
table: "GameTokens");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Used",
|
||||
table: "GameTokens");
|
||||
|
||||
migrationBuilder.AddColumn<ulong>(
|
||||
name: "LinkedPsnId",
|
||||
table: "Users",
|
||||
type: "bigint unsigned",
|
||||
nullable: false,
|
||||
defaultValue: 0ul);
|
||||
|
||||
migrationBuilder.AddColumn<ulong>(
|
||||
name: "LinkedRpcnId",
|
||||
table: "Users",
|
||||
type: "bigint unsigned",
|
||||
nullable: false,
|
||||
defaultValue: 0ul);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TicketHash",
|
||||
table: "GameTokens",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LinkedPsnId",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LinkedRpcnId",
|
||||
table: "Users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TicketHash",
|
||||
table: "GameTokens");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ApprovedIPAddress",
|
||||
table: "Users",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Approved",
|
||||
table: "GameTokens",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Used",
|
||||
table: "GameTokens",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AuthenticationAttempts",
|
||||
columns: table => new
|
||||
{
|
||||
AuthenticationAttemptId = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
GameTokenId = table.Column<int>(type: "int", nullable: false),
|
||||
IPAddress = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Platform = table.Column<int>(type: "int", nullable: false),
|
||||
Timestamp = table.Column<long>(type: "bigint", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AuthenticationAttempts", x => x.AuthenticationAttemptId);
|
||||
table.ForeignKey(
|
||||
name: "FK_AuthenticationAttempts_GameTokens_GameTokenId",
|
||||
column: x => x.GameTokenId,
|
||||
principalTable: "GameTokens",
|
||||
principalColumn: "TokenId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuthenticationAttempts_GameTokenId",
|
||||
table: "AuthenticationAttempts",
|
||||
column: "GameTokenId");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
using LBPUnion.ProjectLighthouse;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ProjectLighthouse.Migrations
|
||||
{
|
||||
[DbContext(typeof(Database))]
|
||||
[Migration("20221217043015_AddPlatformLinkAttempts")]
|
||||
public partial class AddPlatformLinkAttempts : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PlatformLinkAttempts",
|
||||
columns: table => new
|
||||
{
|
||||
PlatformLinkAttemptId = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
UserId = table.Column<int>(type: "int", nullable: false),
|
||||
PlatformId = table.Column<ulong>(type: "bigint unsigned", nullable: false),
|
||||
Platform = table.Column<int>(type: "int", nullable: false),
|
||||
Timestamp = table.Column<long>(type: "bigint", nullable: false),
|
||||
IPAddress = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PlatformLinkAttempts", x => x.PlatformLinkAttemptId);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "PlatformLinkAttempts");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
using LBPUnion.ProjectLighthouse;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ProjectLighthouse.Migrations
|
||||
{
|
||||
[DbContext(typeof(Database))]
|
||||
[Migration("20221217044751_AddForeignKeyToLinkAttempts")]
|
||||
public partial class AddForeignKeyToLinkAttempts : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PlatformLinkAttempts_UserId",
|
||||
table: "PlatformLinkAttempts",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PlatformLinkAttempts_Users_UserId",
|
||||
table: "PlatformLinkAttempts",
|
||||
column: "UserId",
|
||||
principalTable: "Users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PlatformLinkAttempts_Users_UserId",
|
||||
table: "PlatformLinkAttempts");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_PlatformLinkAttempts_UserId",
|
||||
table: "PlatformLinkAttempts");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.PlayerData;
|
||||
|
||||
public class AuthenticationAttempt
|
||||
{
|
||||
[Key]
|
||||
public int AuthenticationAttemptId { get; set; }
|
||||
|
||||
public long Timestamp { get; set; }
|
||||
public Platform Platform { get; set; }
|
||||
public string IPAddress { get; set; }
|
||||
|
||||
public int GameTokenId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(GameTokenId))]
|
||||
public GameToken GameToken { get; set; }
|
||||
}
|
|
@ -24,11 +24,7 @@ public class GameToken
|
|||
|
||||
public Platform Platform { get; set; }
|
||||
|
||||
// Set by /authentication webpage
|
||||
public bool Approved { get; set; }
|
||||
|
||||
// Set to true on login
|
||||
public bool Used { get; set; }
|
||||
public string TicketHash { get; set; }
|
||||
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
}
|
25
ProjectLighthouse/PlayerData/PlatformLinkAttempt.cs
Normal file
25
ProjectLighthouse/PlayerData/PlatformLinkAttempt.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData.Profiles;
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.PlayerData;
|
||||
|
||||
public class PlatformLinkAttempt
|
||||
{
|
||||
[Key]
|
||||
public int PlatformLinkAttemptId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(UserId))]
|
||||
public User User { get; set; }
|
||||
|
||||
public int UserId { get; set; }
|
||||
|
||||
public ulong PlatformId { get; set; }
|
||||
|
||||
public Platform Platform { get; set; }
|
||||
|
||||
public long Timestamp { get; set; }
|
||||
|
||||
public string IPAddress { get; set; } = "";
|
||||
|
||||
}
|
|
@ -87,15 +87,14 @@ public class User
|
|||
private int PhotosWithMe()
|
||||
{
|
||||
List<int> photoSubjectIds = new();
|
||||
photoSubjectIds.AddRange(this.database.PhotoSubjects.Where(p => p.UserId == this.UserId).Select(p => p.PhotoSubjectId));
|
||||
photoSubjectIds.AddRange(this.database.PhotoSubjects.Where(p => p.UserId == this.UserId)
|
||||
.Select(p => p.PhotoSubjectId));
|
||||
|
||||
var list = this.database.Photos.Select(p => new
|
||||
{
|
||||
p.PhotoId,
|
||||
p.PhotoSubjectCollection,
|
||||
}).ToList();
|
||||
List<int> photoIds = (from v in list where photoSubjectIds.Any(ps => v.PhotoSubjectCollection.Split(",").Contains(ps.ToString())) select v.PhotoId).ToList();
|
||||
return this.database.Photos.Count(p => photoIds.Any(pId => p.PhotoId == pId) && p.CreatorId != this.UserId);
|
||||
return (
|
||||
from id in photoSubjectIds
|
||||
from photo in this.database.Photos.Where(p => p.PhotoSubjectCollection.Contains(id.ToString())).ToList()
|
||||
where photo.PhotoSubjectCollection.Split(",").Contains(id.ToString()) && photo.CreatorId != this.UserId
|
||||
select id).Count();
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
|
@ -179,11 +178,6 @@ public class User
|
|||
public string? BannedReason { get; set; }
|
||||
#nullable disable
|
||||
|
||||
#nullable enable
|
||||
[JsonIgnore]
|
||||
public string? ApprovedIPAddress { get; set; }
|
||||
#nullable disable
|
||||
|
||||
[JsonIgnore]
|
||||
public string Language { get; set; } = "en";
|
||||
|
||||
|
@ -207,6 +201,12 @@ public class User
|
|||
[JsonIgnore]
|
||||
public string TwoFactorBackup { get; set; } = "";
|
||||
|
||||
[JsonIgnore]
|
||||
public ulong LinkedRpcnId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public ulong LinkedPsnId { get; set; }
|
||||
|
||||
// should not be adjustable by user
|
||||
public bool CommentsEnabled { get; set; } = true;
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<PackageReference Include="SharpZipLib" Version="1.4.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="12.3.1" />
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace ProjectLighthouse.Migrations
|
|||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "6.0.10")
|
||||
.HasAnnotation("ProductVersion", "7.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.Administration.CompletedMigration", b =>
|
||||
|
@ -457,40 +457,12 @@ namespace ProjectLighthouse.Migrations
|
|||
b.ToTable("APIKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.AuthenticationAttempt", b =>
|
||||
{
|
||||
b.Property<int>("AuthenticationAttemptId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("GameTokenId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("IPAddress")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("Timestamp")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("AuthenticationAttemptId");
|
||||
|
||||
b.HasIndex("GameTokenId");
|
||||
|
||||
b.ToTable("AuthenticationAttempts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.GameToken", b =>
|
||||
{
|
||||
b.Property<int>("TokenId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("Approved")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
|
@ -500,8 +472,8 @@ namespace ProjectLighthouse.Migrations
|
|||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("Used")
|
||||
.HasColumnType("tinyint(1)");
|
||||
b.Property<string>("TicketHash")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
@ -602,6 +574,34 @@ namespace ProjectLighthouse.Migrations
|
|||
b.ToTable("PhotoSubjects");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.PlatformLinkAttempt", b =>
|
||||
{
|
||||
b.Property<int>("PlatformLinkAttemptId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("IPAddress")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("Platform")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<ulong>("PlatformId")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<long>("Timestamp")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("PlatformLinkAttemptId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("PlatformLinkAttempts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Profiles.Comment", b =>
|
||||
{
|
||||
b.Property<int>("CommentId")
|
||||
|
@ -755,9 +755,6 @@ namespace ProjectLighthouse.Migrations
|
|||
b.Property<int>("AdminGrantedSlots")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ApprovedIPAddress")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("BannedReason")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
|
@ -794,6 +791,12 @@ namespace ProjectLighthouse.Migrations
|
|||
b.Property<int>("LevelVisibility")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<ulong>("LinkedPsnId")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<ulong>("LinkedRpcnId")
|
||||
.HasColumnType("bigint unsigned");
|
||||
|
||||
b.Property<int>("LocationId")
|
||||
.HasColumnType("int");
|
||||
|
||||
|
@ -1165,17 +1168,6 @@ namespace ProjectLighthouse.Migrations
|
|||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.AuthenticationAttempt", b =>
|
||||
{
|
||||
b.HasOne("LBPUnion.ProjectLighthouse.PlayerData.GameToken", "GameToken")
|
||||
.WithMany()
|
||||
.HasForeignKey("GameTokenId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("GameToken");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.GameToken", b =>
|
||||
{
|
||||
b.HasOne("LBPUnion.ProjectLighthouse.PlayerData.Profiles.User", "User")
|
||||
|
@ -1215,6 +1207,17 @@ namespace ProjectLighthouse.Migrations
|
|||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.PlatformLinkAttempt", b =>
|
||||
{
|
||||
b.HasOne("LBPUnion.ProjectLighthouse.PlayerData.Profiles.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.Profiles.Comment", b =>
|
||||
{
|
||||
b.HasOne("LBPUnion.ProjectLighthouse.PlayerData.Profiles.User", "Poster")
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using System.Linq;
|
||||
using LBPUnion.ProjectLighthouse.Extensions;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
using LBPUnion.ProjectLighthouse.Logging;
|
||||
using LBPUnion.ProjectLighthouse.PlayerData;
|
||||
using LBPUnion.ProjectLighthouse.Tickets.Data;
|
||||
using Org.BouncyCastle.Asn1;
|
||||
using Org.BouncyCastle.Asn1.X9;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Math;
|
||||
using Org.BouncyCastle.Math.EC;
|
||||
using Org.BouncyCastle.Security;
|
||||
using Version = LBPUnion.ProjectLighthouse.Types.Version;
|
||||
#if DEBUG
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
#endif
|
||||
|
||||
namespace LBPUnion.ProjectLighthouse.Tickets;
|
||||
|
||||
|
@ -27,20 +38,89 @@ public class NPTicket
|
|||
public uint IssuerId { get; set; }
|
||||
public ulong IssuedDate { get; set; }
|
||||
public ulong ExpireDate { get; set; }
|
||||
public ulong UserId { get; set; }
|
||||
public string TicketHash { get; set; } = "";
|
||||
|
||||
private string? titleId { get; set; }
|
||||
|
||||
private byte[] ticketBody { get; set; } = Array.Empty<byte>();
|
||||
|
||||
private byte[] ticketSignature { get; set; } = Array.Empty<byte>();
|
||||
|
||||
public GameVersion GameVersion { get; set; }
|
||||
|
||||
private static void Read21Ticket(NPTicket npTicket, TicketReader reader)
|
||||
private static ECDomainParameters FromX9EcParams(X9ECParameters param) =>
|
||||
new(param.Curve, param.G, param.N, param.H, param.GetSeed());
|
||||
|
||||
private static readonly ECDomainParameters secp224K1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp224k1"));
|
||||
private static readonly ECDomainParameters secp192R1 = FromX9EcParams(ECNamedCurveTable.GetByName("secp192r1"));
|
||||
|
||||
private static readonly ECPoint rpcnPublic = secp224K1.Curve.CreatePoint(
|
||||
new BigInteger("b07bc0f0addb97657e9f389039e8d2b9c97dc2a31d3042e7d0479b93", 16),
|
||||
new BigInteger("d81c42b0abdf6c42191a31e31f93342f8f033bd529c2c57fdb5a0a7d", 16));
|
||||
|
||||
private static readonly ECPoint psnPublic = secp192R1.Curve.CreatePoint(
|
||||
new BigInteger("39c62d061d4ee35c5f3f7531de0af3cf918346526edac727", 16),
|
||||
new BigInteger("a5d578b55113e612bf1878d4cc939d61a41318403b5bdf86", 16));
|
||||
|
||||
private ECDomainParameters getCurveParams() => this.IsRpcn() ? secp224K1 : secp192R1;
|
||||
|
||||
private ECPoint getPublicKey() => this.IsRpcn() ? rpcnPublic : psnPublic;
|
||||
|
||||
private bool ValidateSignature()
|
||||
{
|
||||
reader.ReadTicketString(); // "Serial id", but its apparently not what we're looking for
|
||||
ECPublicKeyParameters pubKey = new(this.getPublicKey(), this.getCurveParams());
|
||||
string algo = this.IsRpcn() ? "SHA-224" : "SHA-1";
|
||||
|
||||
ISigner signer = SignerUtilities.GetSigner($"{algo}withECDSA");
|
||||
signer.Init(false, pubKey);
|
||||
|
||||
signer.BlockUpdate(this.ticketBody);
|
||||
|
||||
return signer.VerifySignature(this.ticketSignature);
|
||||
}
|
||||
|
||||
private bool IsRpcn() => this.IssuerId == 0x33333333;
|
||||
|
||||
private static readonly Dictionary<Platform, byte[]> identifierByPlatform = new()
|
||||
{
|
||||
{
|
||||
Platform.RPCS3, "RPCN"u8.ToArray()
|
||||
},
|
||||
{
|
||||
Platform.PS3, new byte[]{ 0x71, 0x9F, 0x1D, 0x4A, }
|
||||
},
|
||||
};
|
||||
|
||||
// Sometimes psn signatures have one or two extra empty bytes
|
||||
// This is slow but it's better than carelessly chopping 0's
|
||||
private static byte[] ParseSignature(byte[] signature)
|
||||
{
|
||||
for (int i = 0; i <= 2; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Asn1Object.FromByteArray(signature);
|
||||
break;
|
||||
}
|
||||
catch
|
||||
{
|
||||
signature = signature.SkipLast(1).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
private static bool Read21Ticket(NPTicket npTicket, TicketReader reader)
|
||||
{
|
||||
reader.ReadTicketString(); // serial id
|
||||
|
||||
npTicket.IssuerId = reader.ReadTicketUInt32();
|
||||
npTicket.IssuedDate = reader.ReadTicketUInt64();
|
||||
npTicket.ExpireDate = reader.ReadTicketUInt64();
|
||||
|
||||
reader.ReadTicketUInt64(); // PSN User id, we don't care about this
|
||||
npTicket.UserId = reader.ReadTicketUInt64();
|
||||
|
||||
npTicket.Username = reader.ReadTicketString();
|
||||
|
||||
|
@ -48,12 +128,99 @@ public class NPTicket
|
|||
reader.ReadTicketString(); // Domain
|
||||
|
||||
npTicket.titleId = reader.ReadTicketString();
|
||||
|
||||
reader.ReadTicketUInt32(); // status
|
||||
|
||||
reader.ReadTicketEmpty(); // padding
|
||||
reader.ReadTicketEmpty();
|
||||
|
||||
reader.ReadSectionHeader(); // footer header
|
||||
|
||||
byte[] ticketIdent = reader.ReadTicketBinary(); // 4 byte identifier
|
||||
Platform platform = npTicket.IsRpcn() ? Platform.RPCS3 : Platform.PS3;
|
||||
byte[] platformIdent = identifierByPlatform[platform];
|
||||
if (!ticketIdent.SequenceEqual(platformIdent))
|
||||
{
|
||||
Logger.Warn(@$"Identity sequence mismatch, platform={npTicket.Platform} - {Convert.ToHexString(ticketIdent)} == {Convert.ToHexString(platformIdent)}", LogArea.Login);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function is here for future use incase we ever need to read more from the ticket
|
||||
private static void Read30Ticket(NPTicket npTicket, TicketReader reader)
|
||||
npTicket.ticketSignature = ParseSignature(reader.ReadTicketBinary());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool Read30Ticket(NPTicket npTicket, TicketReader reader)
|
||||
{
|
||||
Read21Ticket(npTicket, reader);
|
||||
reader.ReadTicketString(); // serial id
|
||||
|
||||
npTicket.IssuerId = reader.ReadTicketUInt32();
|
||||
npTicket.IssuedDate = reader.ReadTicketUInt64();
|
||||
npTicket.ExpireDate = reader.ReadTicketUInt64();
|
||||
|
||||
npTicket.UserId = reader.ReadTicketUInt64();
|
||||
|
||||
npTicket.Username = reader.ReadTicketString();
|
||||
|
||||
reader.ReadTicketString(); // Country
|
||||
reader.ReadTicketString(); // Domain
|
||||
|
||||
npTicket.titleId = reader.ReadTicketString();
|
||||
|
||||
reader.ReadSectionHeader(); // date of birth section
|
||||
reader.ReadBytes(4); // 4 bytes for year month and day
|
||||
reader.ReadTicketUInt32();
|
||||
|
||||
reader.ReadSectionHeader(); // empty section?
|
||||
reader.ReadTicketEmpty();
|
||||
|
||||
reader.ReadSectionHeader(); // footer header
|
||||
|
||||
byte[] ticketIdent = reader.ReadTicketBinary(); // 4 byte identifier
|
||||
Platform platform = npTicket.IsRpcn() ? Platform.RPCS3 : Platform.PS3;
|
||||
byte[] platformIdent = identifierByPlatform[platform];
|
||||
if (!ticketIdent.SequenceEqual(platformIdent))
|
||||
{
|
||||
Logger.Warn(@$"Identity sequence mismatch, platform={npTicket.Platform} - {Convert.ToHexString(ticketIdent)} == {Convert.ToHexString(platformIdent)}", LogArea.Login);
|
||||
return false;
|
||||
}
|
||||
|
||||
npTicket.ticketSignature = ParseSignature(reader.ReadTicketBinary());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ReadTicket(byte[] data, NPTicket npTicket, TicketReader reader)
|
||||
{
|
||||
npTicket.ticketVersion = reader.ReadTicketVersion();
|
||||
|
||||
reader.ReadBytes(4); // Skip header
|
||||
|
||||
ushort ticketLen = reader.ReadUInt16BE();
|
||||
// Subtract 8 bytes to account for ticket header
|
||||
if (ticketLen != data.Length - 0x8)
|
||||
{
|
||||
Logger.Warn(@$"Ticket length mismatch, expected={ticketLen}, actual={data.Length - 0x8}", LogArea.Login);
|
||||
return false;
|
||||
}
|
||||
|
||||
long bodyStart = reader.BaseStream.Position;
|
||||
SectionHeader bodyHeader = reader.ReadSectionHeader();
|
||||
|
||||
Logger.Debug($"bodyHeader.Type is {bodyHeader.Type}, index={bodyStart}", LogArea.Login);
|
||||
|
||||
bool parsedSuccessfully = npTicket.ticketVersion.ToString() switch
|
||||
{
|
||||
"2.1" => Read21Ticket(npTicket, reader), // used by ps3
|
||||
"3.0" => Read30Ticket(npTicket, reader), // used by ps vita
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
if (!parsedSuccessfully) return false;
|
||||
|
||||
npTicket.ticketBody = npTicket.IsRpcn()
|
||||
? data.AsSpan().Slice((int)bodyStart, bodyHeader.Length + 4).ToArray()
|
||||
: data.AsSpan()[..data.AsSpan().IndexOf(npTicket.ticketSignature)].ToArray();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -76,10 +243,10 @@ public class NPTicket
|
|||
GameVersion = GameVersion.LittleBigPlanet2,
|
||||
ExpireDate = 0,
|
||||
IssuedDate = 0,
|
||||
Username = dataStr["unitTestTicket".Length..],
|
||||
UserId = ulong.Parse(dataStr["unitTestTicketunitTestUser".Length..]),
|
||||
};
|
||||
|
||||
npTicket.Username = dataStr.Substring(14);
|
||||
|
||||
return npTicket;
|
||||
}
|
||||
}
|
||||
|
@ -89,24 +256,22 @@ public class NPTicket
|
|||
using MemoryStream ms = new(data);
|
||||
using TicketReader reader = new(ms);
|
||||
|
||||
npTicket.ticketVersion = reader.ReadTicketVersion();
|
||||
|
||||
reader.ReadBytes(4); // Skip header
|
||||
|
||||
reader.ReadUInt16BE(); // Ticket length, we don't care about this
|
||||
|
||||
SectionHeader bodyHeader = reader.ReadSectionHeader();
|
||||
Logger.Debug($"bodyHeader.Type is {bodyHeader.Type}", LogArea.Login);
|
||||
|
||||
switch (npTicket.ticketVersion)
|
||||
bool validTicket = ReadTicket(data, npTicket, reader);
|
||||
if (!validTicket)
|
||||
{
|
||||
case "2.1":
|
||||
Read21Ticket(npTicket, reader);
|
||||
break;
|
||||
case "3.0":
|
||||
Read30Ticket(npTicket, reader);
|
||||
break;
|
||||
default: throw new NotImplementedException();
|
||||
Logger.Warn($"Failed to parse ticket from {npTicket.Username}", LogArea.Login);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((long)npTicket.IssuedDate > TimeHelper.TimestampMillis)
|
||||
{
|
||||
Logger.Warn($"Ticket isn't valid yet from {npTicket.Username}", LogArea.Login);
|
||||
return null;
|
||||
}
|
||||
if (TimeHelper.TimestampMillis > (long)npTicket.ExpireDate)
|
||||
{
|
||||
Logger.Warn($"Ticket has expired from {npTicket.Username}", LogArea.Login);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (npTicket.titleId == null) throw new ArgumentNullException($"{nameof(npTicket)}.{nameof(npTicket.titleId)}");
|
||||
|
@ -114,8 +279,8 @@ public class NPTicket
|
|||
// We already read the title id, however we need to do some post-processing to get what we want.
|
||||
// Current data: UP9000-BCUS98245_00
|
||||
// We need to chop this to get the titleId we're looking for
|
||||
npTicket.titleId = npTicket.titleId.Substring(7); // Trim UP9000-
|
||||
npTicket.titleId = npTicket.titleId.Substring(0, npTicket.titleId.Length - 3); // Trim _00 at the end
|
||||
npTicket.titleId = npTicket.titleId[7..]; // Trim UP9000-
|
||||
npTicket.titleId = npTicket.titleId[..^3]; // Trim _00 at the end
|
||||
// Data now (hopefully): BCUS98245
|
||||
|
||||
Logger.Debug($"titleId is {npTicket.titleId}", LogArea.Login);
|
||||
|
@ -145,6 +310,15 @@ public class NPTicket
|
|||
return null;
|
||||
}
|
||||
|
||||
if (ServerConfiguration.Instance.Authentication.VerifyTickets && !npTicket.ValidateSignature())
|
||||
{
|
||||
Logger.Warn($"Failed to verify authenticity of ticket from user {npTicket.Username}", LogArea.Login);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Used to identify duplicate tickets
|
||||
npTicket.TicketHash = CryptoHelper.Sha1Hash(data);
|
||||
|
||||
#if DEBUG
|
||||
Logger.Debug("npTicket data:", LogArea.Login);
|
||||
Logger.Debug(JsonSerializer.Serialize(npTicket), LogArea.Login);
|
||||
|
@ -160,7 +334,6 @@ public class NPTicket
|
|||
"Please let us know that this is a ticket version that is actually used on our issue tracker at https://github.com/LBPUnion/project-lighthouse/issues !",
|
||||
LogArea.Login
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
catch(Exception e)
|
||||
|
|
|
@ -19,18 +19,22 @@ public class TicketReader : BinaryReader
|
|||
{
|
||||
this.ReadByte();
|
||||
|
||||
SectionHeader sectionHeader = new();
|
||||
sectionHeader.Type = (SectionType)this.ReadByte();
|
||||
sectionHeader.Length = this.ReadUInt16BE();
|
||||
SectionHeader sectionHeader = new()
|
||||
{
|
||||
Type = (SectionType)this.ReadByte(),
|
||||
Length = this.ReadUInt16BE(),
|
||||
};
|
||||
|
||||
return sectionHeader;
|
||||
}
|
||||
|
||||
public DataHeader ReadDataHeader()
|
||||
private DataHeader ReadDataHeader()
|
||||
{
|
||||
DataHeader dataHeader = new();
|
||||
dataHeader.Type = (DataType)this.ReadUInt16BE();
|
||||
dataHeader.Length = this.ReadUInt16BE();
|
||||
DataHeader dataHeader = new()
|
||||
{
|
||||
Type = (DataType)this.ReadUInt16BE(),
|
||||
Length = this.ReadUInt16BE(),
|
||||
};
|
||||
|
||||
return dataHeader;
|
||||
}
|
||||
|
@ -38,7 +42,7 @@ public class TicketReader : BinaryReader
|
|||
public byte[] ReadTicketBinary()
|
||||
{
|
||||
DataHeader dataHeader = this.ReadDataHeader();
|
||||
Debug.Assert(dataHeader.Type == DataType.Binary || dataHeader.Type == DataType.String);
|
||||
Debug.Assert(dataHeader.Type is DataType.Binary or DataType.String);
|
||||
|
||||
return this.ReadBytes(dataHeader.Length);
|
||||
}
|
||||
|
@ -53,10 +57,16 @@ public class TicketReader : BinaryReader
|
|||
return this.ReadUInt32BE();
|
||||
}
|
||||
|
||||
public void ReadTicketEmpty()
|
||||
{
|
||||
DataHeader dataHeader = this.ReadDataHeader();
|
||||
Debug.Assert(dataHeader.Type == DataType.Empty);
|
||||
}
|
||||
|
||||
public ulong ReadTicketUInt64()
|
||||
{
|
||||
DataHeader dataHeader = this.ReadDataHeader();
|
||||
Debug.Assert(dataHeader.Type == DataType.UInt64 || dataHeader.Type == DataType.Timestamp);
|
||||
Debug.Assert(dataHeader.Type is DataType.UInt64 or DataType.Timestamp);
|
||||
|
||||
return this.ReadUInt64BE();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue