Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to have multiple verified IPs #675

Merged
merged 2 commits into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions Refresh.GameServer/Database/GameDatabaseContext.Tokens.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<GameUserVerifiedIpRelation> 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(() =>
Expand Down
4 changes: 2 additions & 2 deletions Refresh.GameServer/Database/GameDatabaseContext.Users.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions Refresh.GameServer/Database/GameDatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public partial class GameDatabaseContext : RealmDatabaseContext
private RealmDbSet<GamePlaylist> GamePlaylists => new(this._realm);
private RealmDbSet<LevelPlaylistRelation> LevelPlaylistRelations => new(this._realm);
private RealmDbSet<SubPlaylistRelation> SubPlaylistRelations => new(this._realm);
private RealmDbSet<GameUserVerifiedIpRelation> GameUserVerifiedIpRelations => new(this._realm);

internal GameDatabaseContext(IDateTimeProvider time)
{
Expand Down
17 changes: 14 additions & 3 deletions Refresh.GameServer/Database/GameDatabaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -77,6 +77,7 @@ protected GameDatabaseProvider(IDateTimeProvider time)
typeof(EmailVerificationCode),
typeof(QueuedRegistration),
typeof(GameIpVerificationRequest),
typeof(GameUserVerifiedIpRelation),

// assets
typeof(GameAsset),
Expand All @@ -103,8 +104,7 @@ protected override void Migrate(Migration migration, ulong oldVersion)
{
IQueryable<dynamic>? oldUsers = migration.OldRealm.DynamicApi.All("GameUser");
IQueryable<GameUser>? newUsers = migration.NewRealm.All<GameUser>();

if (oldVersion < 145)
if (oldVersion < 161)
for (int i = 0; i < newUsers.Count(); i++)
{
dynamic oldUser = oldUsers.ElementAt(i);
Expand Down Expand Up @@ -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<dynamic>? oldLevels = migration.OldRealm.DynamicApi.All("GameLevel");
Expand Down
43 changes: 34 additions & 9 deletions Refresh.GameServer/Endpoints/ApiV3/AuthenticationApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -193,30 +195,53 @@ public ApiListResponse<ApiGameIpVerificationRequestResponse> 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<ApiGameUserVerifiedIpResponse> GetVerifiedIps(RequestContext context,
GameDatabaseContext database, DataContext dataContext, GameUser user)
{
(int skip, int count) = context.GetPageData();

DatabaseList<GameUserVerifiedIpRelation> verifiedIps = database.GetVerifiedIps(user, skip, count);

return DatabaseList<ApiGameUserVerifiedIpResponse>
.FromOldList<ApiGameUserVerifiedIpResponse, GameUserVerifiedIpRelation>(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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ApiGameUserVerifiedIpResponse, GameUserVerifiedIpRelation>
{
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<ApiGameUserVerifiedIpResponse> FromOldList(IEnumerable<GameUserVerifiedIpRelation> oldList, DataContext dataContext)
=> oldList.Select(r => FromOld(r, dataContext)!);
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public class AuthenticationEndpoints : EndpointGroup
return null;
}

string ipAddress = context.RemoteIp();

TokenPlatform? platform = ticket.DeterminePlatform();

GameUser? user = database.GetUserByUsername(ticket.Username);
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -257,11 +262,18 @@ 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($"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);
}

/// <summary>
/// Called by the game when it exits cleanly.
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions Refresh.GameServer/Types/Relations/GameUserVerifiedIpRelation.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
1 change: 0 additions & 1 deletion Refresh.GameServer/Types/UserData/GameUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Loading