mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-07-28 07:58:40 +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
|
@ -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);
|
||||
if (token == null)
|
||||
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)
|
||||
{
|
||||
Logger.Warn($"Unable to find/generate a token for username {npTicket.Username}", LogArea.Login);
|
||||
return this.StatusCode(403, ""); // If not, then 403.
|
||||
}
|
||||
}
|
||||
ulong targetPlatform = npTicket.Platform == Platform.RPCS3
|
||||
? targetUsername.LinkedRpcnId
|
||||
: targetUsername.LinkedPsnId;
|
||||
|
||||
// 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)
|
||||
{
|
||||
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()
|
||||
// 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()
|
||||
{
|
||||
GameToken = token,
|
||||
GameTokenId = token.TokenId,
|
||||
Timestamp = TimeHelper.Timestamp,
|
||||
IPAddress = ipAddress,
|
||||
Platform = npTicket.Platform,
|
||||
UserId = targetUsername.UserId,
|
||||
IPAddress = ipAddress,
|
||||
Timestamp = TimeHelper.TimestampMillis,
|
||||
PlatformId = npTicket.UserId,
|
||||
};
|
||||
|
||||
this.database.AuthenticationAttempts.Add(authAttempt);
|
||||
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);
|
||||
}
|
||||
else
|
||||
// automatically change username if it doesn't match
|
||||
else if (user.Username != npTicket.Username)
|
||||
{
|
||||
token.Approved = true;
|
||||
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();
|
||||
}
|
||||
|
||||
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.Approved)
|
||||
if (token != null)
|
||||
{
|
||||
Logger.Warn($"Token unapproved for user {user.Username}, rejecting login", LogArea.Login);
|
||||
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 (user.IsBanned)
|
||||
{
|
||||
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)
|
||||
.OrderByDescending(s => s.Timestamp)
|
||||
.Skip(Math.Max(0, pageStart - 1))
|
||||
.Take(Math.Min(pageSize, 30)),
|
||||
string.Empty,
|
||||
string response = photos
|
||||
.OrderByDescending(s => s.Timestamp)
|
||||
.Skip(Math.Max(0, pageStart - 1))
|
||||
.Take(Math.Min(pageSize, 30)).Aggregate(string.Empty,
|
||||
(current, photo) => current + photo.Serialize());
|
||||
|
||||
return this.Ok(LbpSerializer.StringElement("photos", response));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue