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:
Josh 2022-12-26 03:03:14 -06:00 committed by GitHub
parent ff7969a147
commit 19ea44e0e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 836 additions and 449 deletions

View file

@ -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;