From d243edb80447979aa54b4bf2a3179489005578f7 Mon Sep 17 00:00:00 2001 From: Beyley Thomas Date: Sat, 19 Oct 2024 17:01:38 -0700 Subject: [PATCH 1/2] Allow users to have multiple verified IPs --- .../Database/GameDatabaseContext.Tokens.cs | 20 +++++++-- .../Database/GameDatabaseContext.Users.cs | 4 +- .../Database/GameDatabaseContext.cs | 1 + .../Database/GameDatabaseProvider.cs | 17 ++++++-- .../ApiV3/AuthenticationApiEndpoints.cs | 43 +++++++++++++++---- .../Users/ApiGameUserVerifiedIpResponse.cs | 26 +++++++++++ .../Game/Handshake/AuthenticationEndpoints.cs | 21 +++++++-- .../Relations/GameUserVerifiedIpRelation.cs | 13 ++++++ Refresh.GameServer/Types/UserData/GameUser.cs | 1 - 9 files changed, 124 insertions(+), 22 deletions(-) create mode 100644 Refresh.GameServer/Endpoints/ApiV3/DataTypes/Response/Users/ApiGameUserVerifiedIpResponse.cs create mode 100644 Refresh.GameServer/Types/Relations/GameUserVerifiedIpRelation.cs diff --git a/Refresh.GameServer/Database/GameDatabaseContext.Tokens.cs b/Refresh.GameServer/Database/GameDatabaseContext.Tokens.cs index 6c7885aa..16208eb8 100644 --- a/Refresh.GameServer/Database/GameDatabaseContext.Tokens.cs +++ b/Refresh.GameServer/Database/GameDatabaseContext.Tokens.cs @@ -1,6 +1,8 @@ using System.Security.Cryptography; using JetBrains.Annotations; using Refresh.GameServer.Authentication; +using Refresh.GameServer.Time; +using Refresh.GameServer.Types.Relations; using Refresh.GameServer.Types.UserData; namespace Refresh.GameServer.Database; @@ -165,15 +167,27 @@ public void AddIpVerificationRequest(GameUser user, string ipAddress) }); } - public void SetApprovedIp(GameUser user, string ipAddress) + public void AddVerifiedIp(GameUser user, string ipAddress, IDateTimeProvider timeProvider) { this.Write(() => { - user.CurrentVerifiedIp = ipAddress; - this.GameIpVerificationRequests.RemoveRange(r => r.User == user); + this.GameUserVerifiedIpRelations.Add(new GameUserVerifiedIpRelation + { + User = user, + IpAddress = ipAddress, + VerifiedAt = timeProvider.Now, + }); + + this.GameIpVerificationRequests.RemoveRange(r => r.User == user && r.IpAddress == ipAddress); }); } + public DatabaseList GetVerifiedIps(GameUser user, int skip, int count) + => new(this.GameUserVerifiedIpRelations.Where(r => r.User == user), skip, count); + + public bool IsIpVerified(GameUser user, string ipAddress) + => this.GameUserVerifiedIpRelations.Any(r => r.User == user && r.IpAddress == ipAddress); + public void DenyIpVerificationRequest(GameUser user, string ipAddress) { this.Write(() => diff --git a/Refresh.GameServer/Database/GameDatabaseContext.Users.cs b/Refresh.GameServer/Database/GameDatabaseContext.Users.cs index 603b5996..4e7140ea 100644 --- a/Refresh.GameServer/Database/GameDatabaseContext.Users.cs +++ b/Refresh.GameServer/Database/GameDatabaseContext.Users.cs @@ -321,7 +321,6 @@ public void DeleteUser(GameUser user) user.IconHash = "0"; user.AllowIpAuthentication = false; user.EmailAddressVerified = false; - user.CurrentVerifiedIp = null; user.PsnAuthenticationAllowed = false; user.RpcnAuthenticationAllowed = false; @@ -335,7 +334,8 @@ public void DeleteUser(GameUser user) this.FavouriteUserRelations.RemoveRange(r => r.UserFavouriting == user); this.QueueLevelRelations.RemoveRange(r => r.User == user); this.GamePhotos.RemoveRange(p => p.Publisher == user); - + this.GameUserVerifiedIpRelations.RemoveRange(p => p.User == user); + foreach (GameLevel level in this.GameLevels.Where(l => l.Publisher == user)) { level.Publisher = null; diff --git a/Refresh.GameServer/Database/GameDatabaseContext.cs b/Refresh.GameServer/Database/GameDatabaseContext.cs index 17d0b70b..04259ea4 100644 --- a/Refresh.GameServer/Database/GameDatabaseContext.cs +++ b/Refresh.GameServer/Database/GameDatabaseContext.cs @@ -61,6 +61,7 @@ public partial class GameDatabaseContext : RealmDatabaseContext private RealmDbSet GamePlaylists => new(this._realm); private RealmDbSet LevelPlaylistRelations => new(this._realm); private RealmDbSet SubPlaylistRelations => new(this._realm); + private RealmDbSet GameUserVerifiedIpRelations => new(this._realm); internal GameDatabaseContext(IDateTimeProvider time) { diff --git a/Refresh.GameServer/Database/GameDatabaseProvider.cs b/Refresh.GameServer/Database/GameDatabaseProvider.cs index ecefbe41..672724b5 100644 --- a/Refresh.GameServer/Database/GameDatabaseProvider.cs +++ b/Refresh.GameServer/Database/GameDatabaseProvider.cs @@ -34,7 +34,7 @@ protected GameDatabaseProvider(IDateTimeProvider time) this._time = time; } - protected override ulong SchemaVersion => 160; + protected override ulong SchemaVersion => 161; protected override string Filename => "refreshGameServer.realm"; @@ -77,6 +77,7 @@ protected GameDatabaseProvider(IDateTimeProvider time) typeof(EmailVerificationCode), typeof(QueuedRegistration), typeof(GameIpVerificationRequest), + typeof(GameUserVerifiedIpRelation), // assets typeof(GameAsset), @@ -103,8 +104,7 @@ protected override void Migrate(Migration migration, ulong oldVersion) { IQueryable? oldUsers = migration.OldRealm.DynamicApi.All("GameUser"); IQueryable? newUsers = migration.NewRealm.All(); - - if (oldVersion < 145) + if (oldVersion < 161) for (int i = 0; i < newUsers.Count(); i++) { dynamic oldUser = oldUsers.ElementAt(i); @@ -225,6 +225,17 @@ protected override void Migrate(Migration migration, ulong oldVersion) if (oldVersion < 145) newUser.ShowModdedContent = true; + + // In version 161, we allowed users to set multiple verified IPs + if (oldVersion < 161 && newUser.AllowIpAuthentication && oldUser.CurrentVerifiedIp != null) + { + migration.NewRealm.Add(new GameUserVerifiedIpRelation + { + User = newUser, + IpAddress = oldUser.CurrentVerifiedIp, + VerifiedAt = this._time.Now, + }); + } } IQueryable? oldLevels = migration.OldRealm.DynamicApi.All("GameLevel"); diff --git a/Refresh.GameServer/Endpoints/ApiV3/AuthenticationApiEndpoints.cs b/Refresh.GameServer/Endpoints/ApiV3/AuthenticationApiEndpoints.cs index fe42b0c0..5a4bf3f3 100644 --- a/Refresh.GameServer/Endpoints/ApiV3/AuthenticationApiEndpoints.cs +++ b/Refresh.GameServer/Endpoints/ApiV3/AuthenticationApiEndpoints.cs @@ -15,7 +15,9 @@ using Refresh.GameServer.Endpoints.ApiV3.DataTypes.Response.Users; using Refresh.GameServer.Extensions; using Refresh.GameServer.Services; +using Refresh.GameServer.Time; using Refresh.GameServer.Types.Data; +using Refresh.GameServer.Types.Relations; using Refresh.GameServer.Types.Roles; using Refresh.GameServer.Types.UserData; @@ -193,30 +195,53 @@ public ApiListResponse GetVerificationRequ (database.GetIpVerificationRequestsForUser(user, count, skip), dataContext); } - [ApiV3Endpoint("verificationRequests/approve", HttpMethods.Put)] + [ApiV3Endpoint("verifiedIps"), MinimumRole(GameUserRole.Restricted)] + [DocSummary("Retrieves the list of IP addresses that have been verified by the logged in user.")] + public ApiListResponse GetVerifiedIps(RequestContext context, + GameDatabaseContext database, DataContext dataContext, GameUser user) + { + (int skip, int count) = context.GetPageData(); + + DatabaseList verifiedIps = database.GetVerifiedIps(user, skip, count); + + return DatabaseList + .FromOldList(verifiedIps, dataContext); + } + + [ApiV3Endpoint("verificationRequests/approve", HttpMethods.Put), MinimumRole(GameUserRole.Restricted)] [DocSummary("Approves a given IP, and clears all remaining verification requests. Send the IP in the body.")] [DocError(typeof(ApiValidationError), ApiValidationError.IpAddressParseErrorWhen)] [DocRequestBody("127.0.0.1")] - public ApiOkResponse ApproveVerificationRequest(RequestContext context, GameDatabaseContext database, GameUser user, string body) + public ApiOkResponse ApproveVerificationRequest( + RequestContext context, + GameDatabaseContext database, + IDateTimeProvider timeProvider, + GameUser user, + string body) { - bool parsed = IPAddress.TryParse(body, out _); - if (!parsed) return ApiValidationError.IpAddressParseError; + string ipAddress = body.Trim(); + + if (!IPAddress.TryParse(ipAddress, out _)) + return ApiValidationError.IpAddressParseError; - database.SetApprovedIp(user, body.Trim()); + if (!database.IsIpVerified(user, ipAddress)) + database.AddVerifiedIp(user, ipAddress, timeProvider); return new ApiOkResponse(); } - + [ApiV3Endpoint("verificationRequests/deny", HttpMethods.Put)] [DocSummary("Denies all verification requests matching a given IP. Send the IP in the body.")] [DocError(typeof(ApiValidationError), ApiValidationError.IpAddressParseErrorWhen)] [DocRequestBody("127.0.0.1")] public ApiOkResponse DenyVerificationRequest(RequestContext context, GameDatabaseContext database, GameUser user, string body) { - bool parsed = IPAddress.TryParse(body, out _); - if (!parsed) return ApiValidationError.IpAddressParseError; + string ipAddress = body.Trim(); + + if (!IPAddress.TryParse(ipAddress, out _)) + return ApiValidationError.IpAddressParseError; - database.DenyIpVerificationRequest(user, body.Trim()); + database.DenyIpVerificationRequest(user, ipAddress); return new ApiOkResponse(); } diff --git a/Refresh.GameServer/Endpoints/ApiV3/DataTypes/Response/Users/ApiGameUserVerifiedIpResponse.cs b/Refresh.GameServer/Endpoints/ApiV3/DataTypes/Response/Users/ApiGameUserVerifiedIpResponse.cs new file mode 100644 index 00000000..b28148e0 --- /dev/null +++ b/Refresh.GameServer/Endpoints/ApiV3/DataTypes/Response/Users/ApiGameUserVerifiedIpResponse.cs @@ -0,0 +1,26 @@ +using Refresh.GameServer.Types.Data; +using Refresh.GameServer.Types.Relations; + +namespace Refresh.GameServer.Endpoints.ApiV3.DataTypes.Response.Users; + +[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] +public class ApiGameUserVerifiedIpResponse : IApiResponse, IDataConvertableFrom +{ + public required string IpAddress { get; set; } + public required DateTimeOffset VerifiedAt { get; set; } + + public static ApiGameUserVerifiedIpResponse? FromOld(GameUserVerifiedIpRelation? old, DataContext dataContext) + { + if (old == null) + return null; + + return new ApiGameUserVerifiedIpResponse + { + IpAddress = old.IpAddress, + VerifiedAt = old.VerifiedAt, + }; + } + + public static IEnumerable FromOldList(IEnumerable oldList, DataContext dataContext) + => oldList.Select(r => FromOld(r, dataContext)!); +} \ No newline at end of file diff --git a/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs index 5d26ba88..9863c041 100644 --- a/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs @@ -47,6 +47,8 @@ public class AuthenticationEndpoints : EndpointGroup return null; } + string ipAddress = context.RemoteIp(); + TokenPlatform? platform = ticket.DeterminePlatform(); GameUser? user = database.GetUserByUsername(ticket.Username); @@ -135,7 +137,8 @@ public class AuthenticationEndpoints : EndpointGroup { if (!HandleIpAuthentication(context, user, database, !config.UseTicketVerification)) { - context.Logger.LogWarning(BunkumCategory.Authentication, $"Rejecting {user}'s login because their IP was not whitelisted"); + context.Logger.LogWarning(BunkumCategory.Authentication, $"Rejecting {user}'s login from {ipAddress} because the IP was not whitelisted"); + SendIpNotVerifiedNotification(database, user, ipAddress); return null; } } @@ -153,7 +156,7 @@ public class AuthenticationEndpoints : EndpointGroup if (game == TokenGame.LittleBigPlanetVita && platform == TokenPlatform.PS3) platform = TokenPlatform.Vita; else if (game == TokenGame.LittleBigPlanetPSP && platform == TokenPlatform.PS3) platform = TokenPlatform.PSP; - Token token = database.GenerateTokenForUser(user, TokenType.Game, game.Value, platform.Value, context.RemoteIp(), GameDatabaseContext.GameTokenExpirySeconds); // 4 hours + Token token = database.GenerateTokenForUser(user, TokenType.Game, game.Value, platform.Value, ipAddress, GameDatabaseContext.GameTokenExpirySeconds); // 4 hours // Clear the user's force match database.ClearForceMatch(user); @@ -225,7 +228,9 @@ private static bool HandleIpAuthentication(RequestContext context, GameUser user } string address = context.RemoteIp(); - if (address == user.CurrentVerifiedIp) return true; + + if (database.IsIpVerified(user, address)) + return true; database.AddIpVerificationRequest(user, address); return false; @@ -257,11 +262,19 @@ private static void SendVerificationFailureNotification(GameDatabaseContext data private static void SendPlatformNotAllowedNotification(GameDatabaseContext database, GameUser user, TokenPlatform platform) { - database.AddLoginFailNotification($"An authentication attempt was attempted to be made from {platform}, " + + database.AddLoginFailNotification($"An authentication attempt was made from {platform}, " + $"but the respective option for it was disabled. To allow authentication from " + $"{platform}, enable '{platform} Authentication' in settings.", user); } + private static void SendIpNotVerifiedNotification(GameDatabaseContext database, GameUser user, string ipAddress) + { + database.AddLoginFailNotification($"An authentication was made from {ipAddress}, " + + $"but that IP is not verified. If this was not you, " + + $"deny {ipAddress} in settings. If this was you, " + + $"verify {ipAddress} in settings.", user); + } + /// /// Called by the game when it exits cleanly. /// diff --git a/Refresh.GameServer/Types/Relations/GameUserVerifiedIpRelation.cs b/Refresh.GameServer/Types/Relations/GameUserVerifiedIpRelation.cs new file mode 100644 index 00000000..b9fc20ec --- /dev/null +++ b/Refresh.GameServer/Types/Relations/GameUserVerifiedIpRelation.cs @@ -0,0 +1,13 @@ +using Realms; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Types.Relations; + +#nullable disable + +public partial class GameUserVerifiedIpRelation : IRealmObject +{ + public GameUser User { get; set; } + public string IpAddress { get; set; } + public DateTimeOffset VerifiedAt { get; set; } +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/UserData/GameUser.cs b/Refresh.GameServer/Types/UserData/GameUser.cs index 52d84e35..74a61ff8 100644 --- a/Refresh.GameServer/Types/UserData/GameUser.cs +++ b/Refresh.GameServer/Types/UserData/GameUser.cs @@ -68,7 +68,6 @@ public partial class GameUser : IRealmObject, IRateLimitUser public string MehFaceHash { get; set; } = "0"; public bool AllowIpAuthentication { get; set; } - public string? CurrentVerifiedIp { get; set; } public string? BanReason { get; set; } public DateTimeOffset? BanExpiryDate { get; set; } From b119b4e327794c957567950f23ca002cd3289c7b Mon Sep 17 00:00:00 2001 From: jvyden Date: Sat, 19 Oct 2024 20:23:07 -0400 Subject: [PATCH 2/2] Reword ip fail error Signed-off-by: jvyden --- .../Endpoints/Game/Handshake/AuthenticationEndpoints.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs index 9863c041..af4b40fa 100644 --- a/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs @@ -269,10 +269,9 @@ private static void SendPlatformNotAllowedNotification(GameDatabaseContext datab private static void SendIpNotVerifiedNotification(GameDatabaseContext database, GameUser user, string ipAddress) { - database.AddLoginFailNotification($"An authentication was made from {ipAddress}, " + - $"but that IP is not verified. If this was not you, " + - $"deny {ipAddress} in settings. If this was you, " + - $"verify {ipAddress} in settings.", user); + database.AddLoginFailNotification($"A login attempt was detected from IP '{ipAddress}'. " + + "To authorize this IP, please verify it in your settings. " + + "If this wasn't you, please reject the request.", user); } ///