Skip to content

Commit

Permalink
Verify token IP for game requests
Browse files Browse the repository at this point in the history
Temporary rebase for 2.11
  • Loading branch information
jvyden committed Jun 5, 2024
1 parent ff6e0ce commit f50b2b4
Show file tree
Hide file tree
Showing 11 changed files with 77 additions and 30 deletions.
18 changes: 12 additions & 6 deletions Refresh.GameServer/Authentication/GameAuthenticationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ public GameAuthenticationProvider(GameServerConfig? config)

public Token? AuthenticateToken(ListenerContext request, Lazy<IDatabaseContext> db)
{
// first try to grab token data from MM_AUTH
// First try to grab game token data from MM_AUTH
string? tokenData = request.Cookies["MM_AUTH"];
TokenType tokenType = TokenType.Game;

// if this is null, this must be an API request so grab from authorization
// If this is null, this must be an API request so grab from authorization
if (tokenData == null)
{
tokenType = TokenType.Api;
tokenData = request.RequestHeaders["Authorization"];
}

// If still null, then we could not find a token, so bail
if (tokenData == null) return null;

// ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault
string validBaseRoute = tokenType switch
Expand All @@ -40,22 +43,25 @@ public GameAuthenticationProvider(GameServerConfig? config)
_ => throw new ArgumentOutOfRangeException(),
};

// If the request URI does not match the base route of the type of token it is, then bail.
// This is to prevent API tokens from making game requests, and vice versa.
if (!request.Uri.AbsolutePath.StartsWith(validBaseRoute))
return null;

// if still null we dont have a token so bail
if (tokenData == null) return null;

GameDatabaseContext database = (GameDatabaseContext)db.Value;
Debug.Assert(database != null);

Token? token = database.GetTokenFromTokenData(tokenData, tokenType);
if (token == null) return null;

// Don't allow game requests from the wrong IP.
if (tokenType == TokenType.Game && request.RemoteEndpoint.Address.ToString() != token.IpAddress)
return null;

GameUser user = token.User;
user.RateLimitUserId = user.UserId;

// don't allow non-admins to authenticate during maintenance mode.
// Don't allow non-admins to authenticate during maintenance mode.
// technically, this check isn't here for token but this is okay since
// we don't actually receive tokens in endpoints (except during logout, aka token revocation)
if ((this._config?.MaintenanceMode ?? false) && user.Role != GameUserRole.Admin)
Expand Down
2 changes: 2 additions & 0 deletions Refresh.GameServer/Authentication/Token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public TokenGame TokenGame

public DateTimeOffset ExpiresAt { get; set; }
public DateTimeOffset LoginDate { get; set; }

public string IpAddress { get; set; }

public GameUser User { get; set; }
}
3 changes: 2 additions & 1 deletion Refresh.GameServer/Database/GameDatabaseContext.Tokens.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private static string GetTokenString(int length)
return Convert.ToBase64String(tokenData);
}

public Token GenerateTokenForUser(GameUser user, TokenType type, TokenGame game, TokenPlatform platform, int tokenExpirySeconds = DefaultTokenExpirySeconds)
public Token GenerateTokenForUser(GameUser user, TokenType type, TokenGame game, TokenPlatform platform, string ipAddress, int tokenExpirySeconds = DefaultTokenExpirySeconds)
{
// TODO: JWT (JSON Web Tokens) for TokenType.Api

Expand All @@ -48,6 +48,7 @@ public Token GenerateTokenForUser(GameUser user, TokenType type, TokenGame game,
TokenPlatform = platform,
ExpiresAt = this._time.Now.AddSeconds(tokenExpirySeconds),
LoginDate = this._time.Now,
IpAddress = ipAddress,
};

if (user.LastLoginDate == DateTimeOffset.MinValue)
Expand Down
6 changes: 5 additions & 1 deletion 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 => 124;
protected override ulong SchemaVersion => 126;

protected override string Filename => "refreshGameServer.realm";

Expand Down Expand Up @@ -286,6 +286,10 @@ protected override void Migrate(Migration migration, ulong oldVersion)
migration.NewRealm.Remove(eventToNuke);
}

// In version 126, we started tracking token IP, there's no way for us to acquire this after the fact, so lets just clear all the tokens
if (oldVersion < 126)
migration.NewRealm.RemoveAll<Token>();

// IQueryable<dynamic>? oldTokens = migration.OldRealm.DynamicApi.All("Token");
IQueryable<Token>? newTokens = migration.NewRealm.All<Token>();

Expand Down
16 changes: 9 additions & 7 deletions Refresh.GameServer/Endpoints/ApiV3/AuthenticationApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,13 @@ public ApiResponse<IApiAuthenticationResponse> Authenticate(RequestContext conte
return new ApiAuthenticationError(
"The server is currently in maintenance mode, so it is only accessible for administrators. " +
"Check back later.");


string ipAddress = context.RemoteIp();

// if this is a legacy user, have them create a password on login
if (user.PasswordBcrypt == null)
{
Token resetToken = database.GenerateTokenForUser(user, TokenType.PasswordReset, TokenGame.Website, TokenPlatform.Website);
Token resetToken = database.GenerateTokenForUser(user, TokenType.PasswordReset, TokenGame.Website, TokenPlatform.Website, ipAddress);

return new ApiResetPasswordResponse
{
Expand All @@ -87,8 +89,8 @@ public ApiResponse<IApiAuthenticationResponse> Authenticate(RequestContext conte
$"For more information or to request account deletion, please contact the server administrator.\n" +
$"Reason: {user.BanReason}");

Token token = database.GenerateTokenForUser(user, TokenType.Api, TokenGame.Website, TokenPlatform.Website);
Token refreshToken = database.GenerateTokenForUser(user, TokenType.ApiRefresh, TokenGame.Website, TokenPlatform.Website, GameDatabaseContext.RefreshTokenExpirySeconds);
Token token = database.GenerateTokenForUser(user, TokenType.Api, TokenGame.Website, TokenPlatform.Website, ipAddress);
Token refreshToken = database.GenerateTokenForUser(user, TokenType.ApiRefresh, TokenGame.Website, TokenPlatform.Website, ipAddress, GameDatabaseContext.RefreshTokenExpirySeconds);

context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} successfully logged in through the API");

Expand All @@ -111,7 +113,7 @@ public ApiResponse<IApiAuthenticationResponse> RefreshToken(RequestContext conte

GameUser user = refreshToken.User;

Token token = database.GenerateTokenForUser(user, TokenType.Api, TokenGame.Website, TokenPlatform.Website);
Token token = database.GenerateTokenForUser(user, TokenType.Api, TokenGame.Website, TokenPlatform.Website, context.RemoteIp());

context.Logger.LogInfo(BunkumCategory.Authentication, $"{user} successfully refreshed their token through the API");

Expand Down Expand Up @@ -162,7 +164,7 @@ public ApiOkResponse SendPasswordResetEmail(RequestContext context,

context.Logger.LogInfo(RefreshContext.PasswordReset, "Sending a password reset request email to {0}.", user.Username);

Token token = database.GenerateTokenForUser(user, TokenType.PasswordReset, TokenGame.Website, TokenPlatform.Website);
Token token = database.GenerateTokenForUser(user, TokenType.PasswordReset, TokenGame.Website, TokenPlatform.Website, context.RemoteIp());
context.Logger.LogTrace(RefreshContext.PasswordReset, "Reset token: {0}", token.TokenData);
smtpService.SendPasswordResetRequest(user, token.TokenData);

Expand Down Expand Up @@ -283,7 +285,7 @@ public ApiResponse<IApiAuthenticationResponse> Register(RequestContext context,
database.VerifyUserEmail(user);
}

Token token = database.GenerateTokenForUser(user, TokenType.Api, TokenGame.Website, TokenPlatform.Website);
Token token = database.GenerateTokenForUser(user, TokenType.Api, TokenGame.Website, TokenPlatform.Website, context.RemoteIp());

return new ApiAuthenticationResponse
{
Expand Down
1 change: 0 additions & 1 deletion Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ public Response DeleteLevelComment(RequestContext context, GameDatabaseContext d
return OK;
}


[GameEndpoint("rateComment/user/{content}", HttpMethods.Post)] // `user` level comments
[GameEndpoint("rateComment/developer/{content}", HttpMethods.Post)] // `developer` level comments
[GameEndpoint("rateUserComment/{content}", HttpMethods.Post)] // profile comments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Database;
using Refresh.GameServer.Extensions;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.Matching;
using Refresh.GameServer.Types.Roles;
Expand Down Expand Up @@ -161,7 +162,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, GameDatabaseContext.GameTokenExpirySeconds); // 4 hours
Token token = database.GenerateTokenForUser(user, TokenType.Game, game.Value, platform.Value, context.RemoteIp(), GameDatabaseContext.GameTokenExpirySeconds); // 4 hours

//Clear the user's force match
database.ClearForceMatch(user);
Expand Down Expand Up @@ -219,7 +220,7 @@ private static bool HandleIpAuthentication(RequestContext context, GameUser user
return false;
}

string address = ((IPEndPoint)context.RemoteEndpoint).Address.ToString();
string address = context.RemoteIp();
if (address == user.CurrentVerifiedIp) return true;

database.AddIpVerificationRequest(user, address);
Expand Down
4 changes: 4 additions & 0 deletions Refresh.GameServer/Extensions/RequestContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Net;
using JetBrains.Annotations;
using Bunkum.Core;

Expand Down Expand Up @@ -27,4 +28,7 @@ public static (int, int) GetPageData(this RequestContext context)

[Pure]
public static bool IsApi(this RequestContext context) => context.Url.AbsolutePath.StartsWith("/api/");

[Pure]
public static string RemoteIp(this RequestContext context) => ((IPEndPoint)context.RemoteEndpoint).Address.ToString();
}
22 changes: 13 additions & 9 deletions RefreshTests.GameServer/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,24 @@ public TestContext(Lazy<TestRefreshGameServer> server, GameDatabaseContext datab

public HttpClient GetAuthenticatedClient(TokenType type,
GameUser? user = null,
int tokenExpirySeconds = GameDatabaseContext.DefaultTokenExpirySeconds)
int tokenExpirySeconds = GameDatabaseContext.DefaultTokenExpirySeconds,
string? ipAddress = null)
{
return this.GetAuthenticatedClient(type, out _, user, tokenExpirySeconds);
return this.GetAuthenticatedClient(type, out _, user, tokenExpirySeconds, ipAddress);
}

public HttpClient GetAuthenticatedClient(TokenType type, TokenGame game, TokenPlatform platform,
GameUser? user = null,
int tokenExpirySeconds = GameDatabaseContext.DefaultTokenExpirySeconds)
int tokenExpirySeconds = GameDatabaseContext.DefaultTokenExpirySeconds,
string? ipAddress = null)
{
return this.GetAuthenticatedClient(type, game, platform, out _, user, tokenExpirySeconds);
return this.GetAuthenticatedClient(type, game, platform, out _, user, tokenExpirySeconds, ipAddress);
}

public HttpClient GetAuthenticatedClient(TokenType type, out string tokenData,
GameUser? user = null,
int tokenExpirySeconds = GameDatabaseContext.DefaultTokenExpirySeconds)
int tokenExpirySeconds = GameDatabaseContext.DefaultTokenExpirySeconds,
string? ipAddress = null)
{
user ??= this.CreateUser();

Expand All @@ -65,16 +68,17 @@ public HttpClient GetAuthenticatedClient(TokenType type, out string tokenData,
_ => TokenPlatform.Website,
};

return this.GetAuthenticatedClient(type, game, platform, out tokenData, user, tokenExpirySeconds);
return this.GetAuthenticatedClient(type, game, platform, out tokenData, user, tokenExpirySeconds, ipAddress);
}

public HttpClient GetAuthenticatedClient(TokenType type, TokenGame game, TokenPlatform platform, out string tokenData,
GameUser? user = null,
int tokenExpirySeconds = GameDatabaseContext.DefaultTokenExpirySeconds)
int tokenExpirySeconds = GameDatabaseContext.DefaultTokenExpirySeconds,
string? ipAddress = null)
{
user ??= this.CreateUser();

Token token = this.Database.GenerateTokenForUser(user, type, game, platform, tokenExpirySeconds);
Token token = this.Database.GenerateTokenForUser(user, type, game, platform, ipAddress ?? "0.0.0.0", tokenExpirySeconds);
tokenData = token.TokenData;

HttpClient client = this.Listener.GetClient();
Expand Down Expand Up @@ -106,7 +110,7 @@ public GameUser CreateAdmin(string? username = null)

public Token CreateToken(GameUser user, TokenType type = TokenType.Game, TokenGame game = TokenGame.LittleBigPlanet2, TokenPlatform platform = TokenPlatform.PS3)
{
return this.Database.GenerateTokenForUser(user, type, game, platform);
return this.Database.GenerateTokenForUser(user, type, game, platform, "0.0.0.0");
}

public GameLevel CreateLevel(GameUser author, string title = "Level", TokenGame gameVersion = TokenGame.LittleBigPlanet1)
Expand Down
24 changes: 24 additions & 0 deletions RefreshTests.GameServer/Tests/Authentication/TokenAbuseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,28 @@ public void CantUseApiTokenOnGame()
request = gameClient.GetAsync("/lbp/eula").Result;
Assert.That(request.StatusCode, Is.EqualTo(OK));
}

[Test]
public void CantUseGameTokenFromWrongIp()
{
using TestContext context = this.GetServer();
GameUser user = context.CreateUser();

using HttpClient gameClient = context.GetAuthenticatedClient(TokenType.Game, user, ipAddress: "256.727.272.636");

HttpResponseMessage request = gameClient.GetAsync("/lbp/eula").Result;
Assert.That(request.StatusCode, Is.EqualTo(Forbidden));
}

[Test]
public void CanUseApiTokenFromWrongIp()
{
using TestContext context = this.GetServer();
GameUser user = context.CreateUser();

using HttpClient gameClient = context.GetAuthenticatedClient(TokenType.Api, user, ipAddress: "256.727.272.636");

HttpResponseMessage request = gameClient.GetAsync("/api/v3/users/me").Result;
Assert.That(request.StatusCode, Is.EqualTo(OK));
}
}
6 changes: 3 additions & 3 deletions RefreshTests.GameServer/Tests/Authentication/TokenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public void TokensExpire()
GameUser user = context.CreateUser();

const int expirySeconds = GameDatabaseContext.DefaultTokenExpirySeconds;
Token token = context.Database.GenerateTokenForUser(user, TokenType.Api, TokenGame.Website, TokenPlatform.Website, expirySeconds);
Token token = context.Database.GenerateTokenForUser(user, TokenType.Api, TokenGame.Website, TokenPlatform.Website, "0.0.0.0", expirySeconds);

Assert.That(context.Database.GetTokenFromTokenData(token.TokenData, TokenType.Api), Is.Not.Null);
context.Time.TimestampMilliseconds = expirySeconds * 1000;
Expand All @@ -27,8 +27,8 @@ public void DoesntGetTokenWithBadTokenData()
{
using TestContext context = this.GetServer(false);

// make a token so that the test doesnt pass cause we have no tokens
context.Database.GenerateTokenForUser(context.CreateUser(), TokenType.Api, TokenGame.Website, TokenPlatform.Website);
// make a token so that the test doesn't pass because we have no tokens
context.Database.GenerateTokenForUser(context.CreateUser(), TokenType.Api, TokenGame.Website, TokenPlatform.Website, "0.0.0.0");

Token? token = context.Database.GetTokenFromTokenData("bad token data", TokenType.Api);
GameUser? user = context.Database.GetUserFromTokenData("bad token data", TokenType.Api);
Expand Down

0 comments on commit f50b2b4

Please sign in to comment.