From e12faea34bf5bfb881f4fd85b8671e8628d8b50f Mon Sep 17 00:00:00 2001 From: turecross321 <51852312+turecross321@users.noreply.github.com> Date: Tue, 29 Aug 2023 22:07:12 +0200 Subject: [PATCH] Implement NpTicket Game Authentication (#141) --- .../Configuration/GameServerConfig.cs | 8 +- .../Database/GameDatabaseContext.GameIps.cs | 104 ++++++++++++ .../GameDatabaseContext.IpAuthorizations.cs | 106 ------------ .../Database/GameDatabaseContext.Sessions.cs | 11 +- .../Database/GameDatabaseContext.Users.cs | 15 ++ .../Database/GameDatabaseProvider.cs | 25 ++- .../Documentation/Errors/ConflictError.cs | 2 +- .../Account/ApiAccountManagementEndpoints.cs | 6 +- .../Api/Account/ApiAuthenticationEndpoints.cs | 56 +++++++ .../Api/ApiIpAuthorizationEndpoints.cs | 57 ------- .../Endpoints/Game/AuthenticationEndpoints.cs | 155 ++++++++++++------ .../Endpoints/Game/EulaEndpoint.cs | 2 +- SoundShapesServer/GameServer.cs | 2 +- SoundShapesServer/Helpers/IpHelper.cs | 6 +- SoundShapesServer/Helpers/PaginationHelper.cs | 2 +- ...Request.cs => ApiAuthenticateIpRequest.cs} | 2 +- .../Requests/Api/ApiCreateAlbumRequest.cs | 1 - .../Api/ApiCreateCommunityTabRequest.cs | 5 +- .../Api/ApiCreateDailyLevelRequest.cs | 1 - .../Requests/Api/ApiCreateNewsEntryRequest.cs | 1 - .../Requests/Api/ApiPunishRequest.cs | 1 - .../Requests/Api/ApiReportRequest.cs | 1 - ...ApiSetGameAuthenticationSettingsRequest.cs | 10 ++ .../ApiGameAuthenticationSettingsResponse.cs | 17 ++ .../Responses/Api/ApiIpResponse.cs | 12 +- SoundShapesServer/SoundShapesServer.csproj | 1 + .../Types/{IpAuthorization.cs => GameIp.cs} | 8 +- .../Types/Sessions/GameSession.cs | 2 +- SoundShapesServer/Types/Users/GameUser.cs | 7 +- .../Verification/SoundShapesSigningKey.cs | 12 ++ SoundShapesServerTests/TestContext.cs | 14 +- 31 files changed, 377 insertions(+), 275 deletions(-) create mode 100644 SoundShapesServer/Database/GameDatabaseContext.GameIps.cs delete mode 100644 SoundShapesServer/Database/GameDatabaseContext.IpAuthorizations.cs delete mode 100644 SoundShapesServer/Endpoints/Api/ApiIpAuthorizationEndpoints.cs rename SoundShapesServer/Requests/Api/{ApiAuthorizeIpRequest.cs => ApiAuthenticateIpRequest.cs} (85%) create mode 100644 SoundShapesServer/Requests/Api/ApiSetGameAuthenticationSettingsRequest.cs create mode 100644 SoundShapesServer/Responses/Api/ApiGameAuthenticationSettingsResponse.cs rename SoundShapesServer/Types/{IpAuthorization.cs => GameIp.cs} (75%) create mode 100644 SoundShapesServer/Verification/SoundShapesSigningKey.cs diff --git a/SoundShapesServer/Configuration/GameServerConfig.cs b/SoundShapesServer/Configuration/GameServerConfig.cs index c80626b1..d88cd2bd 100644 --- a/SoundShapesServer/Configuration/GameServerConfig.cs +++ b/SoundShapesServer/Configuration/GameServerConfig.cs @@ -5,7 +5,7 @@ namespace SoundShapesServer.Configuration; public class GameServerConfig : Config { - public override int CurrentConfigVersion => 4; + public override int CurrentConfigVersion => 5; public override int Version { get; set; } protected override void Migrate(int oldVer, dynamic oldConfig) @@ -14,11 +14,6 @@ protected override void Migrate(int oldVer, dynamic oldConfig) { RequireAuthentication = (bool)oldConfig.ApiAuthentication; } - - if (CurrentConfigVersion < 4) - { - RateLimitSettings.Bucket = RateLimitSettings.DefaultBucket; - } } public string EulaText { get; set; } = "Welcome back to Sound Shapes!"; @@ -31,5 +26,4 @@ protected override void Migrate(int oldVer, dynamic oldConfig) public string EmailHost { get; set; } = "smtp.gmail.com"; public int EmailHostPort { get; set; } = 587; public bool EmailSsl { get; set; } = true; - public RateLimitSettings RateLimitSettings = new (30, 400, 0, RateLimitSettings.DefaultBucket); } \ No newline at end of file diff --git a/SoundShapesServer/Database/GameDatabaseContext.GameIps.cs b/SoundShapesServer/Database/GameDatabaseContext.GameIps.cs new file mode 100644 index 00000000..ac97fcdb --- /dev/null +++ b/SoundShapesServer/Database/GameDatabaseContext.GameIps.cs @@ -0,0 +1,104 @@ +using SoundShapesServer.Helpers; +using SoundShapesServer.Types; +using SoundShapesServer.Types.Sessions; +using SoundShapesServer.Types.Users; + +namespace SoundShapesServer.Database; + +public partial class GameDatabaseContext +{ + private GameIp CreateIpAddress(GameUser user, string ipAddress) + { + GameIp gameIp = new(ipAddress, user); + + _realm.Write(() => + { + _realm.Add(gameIp); + }); + + return gameIp; + } + + public GameIp GetIpFromAddress(GameUser user, string ipAddress) + { + _realm.Refresh(); + + GameIp? ip = user.IpAddresses.FirstOrDefault(i => i.IpAddress == ipAddress); + return ip ?? CreateIpAddress(user, ipAddress); + } + public bool AuthorizeIpAddress(GameIp gameIp, bool oneTime) + { + if (gameIp.Authorized) return false; + + _realm.Write(() => + { + gameIp.Authorized = true; + gameIp.OneTimeUse = oneTime; + gameIp.ModificationDate = DateTimeOffset.UtcNow; + + foreach (GameSession session in gameIp.Sessions.Where(s=>s._SessionType == (int)SessionType.GameUnAuthorized)) + { + session.SessionType = SessionType.Game; + } + }); + + _realm.Refresh(); + + return true; + } + + public void RemoveIpAddress(GameIp gameIp) + { + _realm.Write(() => + { + // Remove all sessions with ip address + foreach (GameSession session in gameIp.Sessions) + { + _realm.Remove(session); + } + + _realm.Remove(gameIp); + }); + + _realm.Refresh(); + } + + public void UseOneTimeIpAddress(GameIp gameIp) + { + _realm.Write(() => + { + gameIp.Authorized = false; + gameIp.OneTimeUse = false; + gameIp.ModificationDate = DateTimeOffset.UtcNow; + }); + } + + public (GameIp[], int) GetPaginatedIps(GameUser user, bool? authorized, int from, int count) + { + IQueryable filteredAddresses = GetIps(user, authorized); + GameIp[] paginatedAddresses = PaginationHelper.PaginateIpAddresses(filteredAddresses, from, count); + + return (paginatedAddresses, filteredAddresses.Count()); + } + + private IQueryable GetIps(GameUser user, bool? authorized) + { + IQueryable addresses = _realm.All().Where(i => i.User == user); + IQueryable filteredAddresses = FilterIpAddresses(addresses, authorized); + + return filteredAddresses; + } + + private static IQueryable FilterIpAddresses(IQueryable addresses, + bool? authorized) + { + IQueryable response = addresses; + + if (authorized != null) + { + response = response.Where(i => i.Authorized == authorized); + } + + return response; + } +} \ No newline at end of file diff --git a/SoundShapesServer/Database/GameDatabaseContext.IpAuthorizations.cs b/SoundShapesServer/Database/GameDatabaseContext.IpAuthorizations.cs deleted file mode 100644 index 076678bd..00000000 --- a/SoundShapesServer/Database/GameDatabaseContext.IpAuthorizations.cs +++ /dev/null @@ -1,106 +0,0 @@ -using SoundShapesServer.Helpers; -using SoundShapesServer.Types; -using SoundShapesServer.Types.Sessions; -using SoundShapesServer.Types.Users; - -namespace SoundShapesServer.Database; - -public partial class GameDatabaseContext -{ - private IpAuthorization CreateIpAddress(GameUser user, string ipAddress) - { - IpAuthorization ip = new(ipAddress, user); - - _realm.Write(() => - { - _realm.Add(ip); - }); - - return ip; - } - - public IpAuthorization GetIpFromAddress(GameUser user, string ipAddress) - { - _realm.Refresh(); - - IpAuthorization? ip = user.IpAddresses.FirstOrDefault(i => i.IpAddress == ipAddress); - return ip ?? CreateIpAddress(user, ipAddress); - } - public bool AuthorizeIpAddress(IpAuthorization ip, bool oneTime) - { - if (ip.Authorized) return false; - - _realm.Write(() => - { - ip.Authorized = true; - ip.OneTimeUse = oneTime; - ip.ModificationDate = DateTimeOffset.UtcNow; - - foreach (GameSession session in ip.Sessions.Where(s=>s._SessionType == (int)SessionType.GameUnAuthorized)) - { - session.SessionType = SessionType.Game; - } - }); - - _realm.Refresh(); - - return true; - } - - public void RemoveIpAddress(IpAuthorization ip) - { - IEnumerable sessionsFromIp = GetSessionsWithIp(ip); - - _realm.Write(() => - { - // Remove all sessions with ip address - foreach (var session in sessionsFromIp) - { - _realm.Remove(session); - } - - _realm.Remove(ip); - }); - - _realm.Refresh(); - } - - public void UseOneTimeIpAddress(IpAuthorization ip) - { - _realm.Write(() => - { - ip.Authorized = false; - ip.OneTimeUse = false; - ip.ModificationDate = DateTimeOffset.UtcNow; - }); - } - - public (IpAuthorization[], int) GetPaginatedIps(GameUser user, bool? authorized, int from, int count) - { - IQueryable filteredAddresses = GetIps(user, authorized); - IpAuthorization[] paginatedAddresses = PaginationHelper.PaginateIpAddresses(filteredAddresses, from, count); - - return (paginatedAddresses, filteredAddresses.Count()); - } - - private IQueryable GetIps(GameUser user, bool? authorized) - { - IQueryable addresses = _realm.All().Where(i => i.User == user); - IQueryable filteredAddresses = FilterIpAddresses(addresses, authorized); - - return filteredAddresses; - } - - private static IQueryable FilterIpAddresses(IQueryable addresses, - bool? authorized) - { - IQueryable response = addresses; - - if (authorized != null) - { - response = response.Where(i => i.Authorized == authorized); - } - - return response; - } -} \ No newline at end of file diff --git a/SoundShapesServer/Database/GameDatabaseContext.Sessions.cs b/SoundShapesServer/Database/GameDatabaseContext.Sessions.cs index 53caf8ed..9c00a54a 100644 --- a/SoundShapesServer/Database/GameDatabaseContext.Sessions.cs +++ b/SoundShapesServer/Database/GameDatabaseContext.Sessions.cs @@ -9,7 +9,7 @@ public partial class GameDatabaseContext public const int DefaultSessionExpirySeconds = Globals.OneDayInSeconds; private const int SimultaneousSessionsLimit = 3; - public GameSession CreateSession(GameUser user, SessionType sessionType, PlatformType platformType, int? expirationSeconds = null, string? id = null, IpAuthorization? ip = null) + public GameSession CreateSession(GameUser user, SessionType sessionType, PlatformType platformType, bool? genuineTicket = null, int? expirationSeconds = null, string? id = null) { double sessionExpirationSeconds = expirationSeconds ?? DefaultSessionExpirySeconds; id ??= GenerateGuid(); @@ -20,9 +20,9 @@ public GameSession CreateSession(GameUser user, SessionType sessionType, Platfor User = user, SessionType = sessionType, PlatformType = platformType, - Ip = ip, ExpiryDate = DateTimeOffset.UtcNow.AddSeconds(sessionExpirationSeconds), - CreationDate = DateTimeOffset.UtcNow + CreationDate = DateTimeOffset.UtcNow, + GenuineNpTicket = genuineTicket }; IEnumerable sessionsToDelete = _realm.All() @@ -47,11 +47,6 @@ public GameSession CreateSession(GameUser user, SessionType sessionType, Platfor return session; } - private IEnumerable GetSessionsWithIp(IpAuthorization ip) - { - return _realm.All().Where(s => s.Ip == ip); - } - public void RemoveSession(GameSession session) { _realm.Write(() => diff --git a/SoundShapesServer/Database/GameDatabaseContext.Users.cs b/SoundShapesServer/Database/GameDatabaseContext.Users.cs index 10a8d467..a4db994e 100644 --- a/SoundShapesServer/Database/GameDatabaseContext.Users.cs +++ b/SoundShapesServer/Database/GameDatabaseContext.Users.cs @@ -1,8 +1,10 @@ using Bunkum.HttpServer.Storage; +using SoundShapesServer.Requests.Api; using SoundShapesServer.Types; using SoundShapesServer.Types.Events; using SoundShapesServer.Types.Levels; using SoundShapesServer.Types.Relations; +using SoundShapesServer.Types.Sessions; using SoundShapesServer.Types.Users; using static SoundShapesServer.Helpers.PaginationHelper; @@ -160,6 +162,19 @@ public void SetFeaturedLevel(GameUser user, GameLevel level) user.FeaturedLevel = level; }); } + + public void SetUserGameAuthenticationSettings(GameUser user, ApiSetGameAuthenticationSettingsRequest request) + { + _realm.Write(() => + { + if (user.AllowIpAuthentication && !request.AllowIpAuthentication) + _realm.RemoveRange(user.IpAddresses); + + user.AllowPsnAuthentication = request.AllowPsnAuthentication; + user.AllowRpcnAuthentication = request.AllowRpcnAuthentication; + user.AllowIpAuthentication = request.AllowIpAuthentication; + }); + } public GameUser? GetUserWithUsername(string username, bool includeDeleted = false) { diff --git a/SoundShapesServer/Database/GameDatabaseProvider.cs b/SoundShapesServer/Database/GameDatabaseProvider.cs index 8211fd75..d3935f6c 100644 --- a/SoundShapesServer/Database/GameDatabaseProvider.cs +++ b/SoundShapesServer/Database/GameDatabaseProvider.cs @@ -17,7 +17,7 @@ namespace SoundShapesServer.Database; public class GameDatabaseProvider : RealmDatabaseProvider { - protected override ulong SchemaVersion => 59; + protected override ulong SchemaVersion => 62; protected override List SchemaTypes => new() { @@ -27,7 +27,7 @@ public class GameDatabaseProvider : RealmDatabaseProvider typeof(LevelPlayRelation), typeof(LevelUniquePlayRelation), typeof(GameUser), - typeof(IpAuthorization), + typeof(GameIp), typeof(GameSession), typeof(GameLevel), typeof(NewsEntry), @@ -92,12 +92,12 @@ protected override void Migrate(Migration migration, ulong oldVersion) IQueryable oldLevels = migration.OldRealm.DynamicApi.All("GameLevel"); IQueryable newLevels = migration.NewRealm.All(); - for (int i = 0; i < newLevels.Count(); i++) + if (oldVersion < 59) { - GameLevel newLevel = newLevels.ElementAt(i); - - if (oldVersion < 59) + for (int i = 0; i < newLevels.Count(); i++) { + GameLevel newLevel = newLevels.ElementAt(i); + Console.WriteLine("Performing Level UploadPlatform Migration. This may take a while. (" + i + "/" + newLevels.Count() + ")"); // Implemented UploadPlatform newLevel.UploadPlatform = PlatformType.Unknown; @@ -123,7 +123,7 @@ protected override void Migrate(Migration migration, ulong oldVersion) { GameSession newSession = newSessions.ElementAt(i); - if (oldVersion < 40) + if (oldVersion < 62) { migration.NewRealm.Remove(newSession); } @@ -278,5 +278,16 @@ protected override void Migrate(Migration migration, ulong oldVersion) newLevel.ModificationDate = newLevel.Date; } } + + IQueryable newIps = migration.NewRealm.All(); + for (int i = 0; i < newIps.Count(); i++) + { + GameIp newIp = newIps.ElementAt(i); + + if (oldVersion < 62) + { + migration.NewRealm.Remove(newIp); + } + } } } \ No newline at end of file diff --git a/SoundShapesServer/Documentation/Errors/ConflictError.cs b/SoundShapesServer/Documentation/Errors/ConflictError.cs index ee8a1499..32ddb9e1 100644 --- a/SoundShapesServer/Documentation/Errors/ConflictError.cs +++ b/SoundShapesServer/Documentation/Errors/ConflictError.cs @@ -8,5 +8,5 @@ public class ConflictError public const string AlreadyLikedLevelWhen = "You have already liked this level."; public const string AlreadyQueuedLevelWhen = "You have already queued this level."; public const string AlreadyFollowingWhen = "You are already following this user."; - public const string AlreadyAuthorizedIpWhen = "You have already authorized this IP."; + public const string AlreadyAuthenticatedIpWhen = "You have already authenticated this IP."; } \ No newline at end of file diff --git a/SoundShapesServer/Endpoints/Api/Account/ApiAccountManagementEndpoints.cs b/SoundShapesServer/Endpoints/Api/Account/ApiAccountManagementEndpoints.cs index 2bd953bf..5f510818 100644 --- a/SoundShapesServer/Endpoints/Api/Account/ApiAccountManagementEndpoints.cs +++ b/SoundShapesServer/Endpoints/Api/Account/ApiAccountManagementEndpoints.cs @@ -46,7 +46,7 @@ public Response SetUsername(RequestContext context, GameDatabaseContext database public Response SendEmailSession(RequestContext context, GameDatabaseContext database, GameUser user, EmailService emailService) { string emailSessionId = GenerateEmailSessionId(database); - GameSession emailSession = database.CreateSession(user, SessionType.SetEmail, PlatformType.Api, Globals.TenMinutesInSeconds, emailSessionId); + GameSession emailSession = database.CreateSession(user, SessionType.SetEmail, PlatformType.Api, null, Globals.TenMinutesInSeconds, emailSessionId); string emailBody = $"Dear {user.Username},\n\n" + "Here is your new email code: " + emailSession.Id + "\n" + @@ -98,7 +98,7 @@ public Response SendPasswordSession(RequestContext context, GameDatabaseContext if (user == null) return HttpStatusCode.Created; string passwordSessionId = GeneratePasswordSessionId(database); - GameSession passwordSession = database.CreateSession(user, SessionType.SetPassword, PlatformType.Api, Globals.TenMinutesInSeconds, passwordSessionId); + GameSession passwordSession = database.CreateSession(user, SessionType.SetPassword, PlatformType.Api, null, Globals.TenMinutesInSeconds, passwordSessionId); string emailBody = $"Dear {user.Username},\n\n" + "Here is your password code: " + passwordSession.Id + "\n" + @@ -138,7 +138,7 @@ public Response SetUserPassword(RequestContext context, GameDatabaseContext data public Response SendUserRemovalSession(RequestContext context, GameDatabaseContext database, GameUser user, GameSession session, EmailService emailService) { string removalSessionId = GenerateAccountRemovalSessionId(database); - GameSession removalSession = database.CreateSession(user, SessionType.RemoveAccount, PlatformType.Api, Globals.TenMinutesInSeconds, removalSessionId); + GameSession removalSession = database.CreateSession(user, SessionType.RemoveAccount, PlatformType.Api, null, Globals.TenMinutesInSeconds, removalSessionId); string emailBody = $"Dear {user.Username},\n\n" + "Here is your account removal code: " + removalSession.Id + "\n" + diff --git a/SoundShapesServer/Endpoints/Api/Account/ApiAuthenticationEndpoints.cs b/SoundShapesServer/Endpoints/Api/Account/ApiAuthenticationEndpoints.cs index d7371b37..00a39ce7 100644 --- a/SoundShapesServer/Endpoints/Api/Account/ApiAuthenticationEndpoints.cs +++ b/SoundShapesServer/Endpoints/Api/Account/ApiAuthenticationEndpoints.cs @@ -5,7 +5,10 @@ using Bunkum.HttpServer.Endpoints; using Bunkum.HttpServer.Responses; using SoundShapesServer.Database; +using SoundShapesServer.Documentation.Attributes; using SoundShapesServer.Documentation.Errors; +using SoundShapesServer.Helpers; +using SoundShapesServer.Requests.Api; using SoundShapesServer.Requests.Api.Account; using SoundShapesServer.Responses.Api; using SoundShapesServer.Types; @@ -43,4 +46,57 @@ public Response Logout(RequestContext context, GameDatabaseContext database, Gam database.RemoveSession(session); return HttpStatusCode.NoContent; } + + // Game Authentication + + [ApiEndpoint("gameAuth/settings", Method.Post)] + [DocSummary("Sets user's game authentication settings.")] + public Response SetGameAuthenticationSettings(RequestContext context, GameDatabaseContext database, GameUser user, ApiSetGameAuthenticationSettingsRequest body) + { + database.SetUserGameAuthenticationSettings(user, body); + return HttpStatusCode.Created; + } + + [ApiEndpoint("gameAuth/settings")] + [DocSummary("Lists user's game authentication settings.")] + public ApiGameAuthenticationSettingsResponse GetGameAuthenticationSettings(RequestContext context, GameUser user) => new (user); + + [ApiEndpoint("gameAuth/ip/authorize", Method.Post)] + [DocSummary("Authorizes specified IP address.")] + [DocError(typeof(ConflictError), ConflictError.AlreadyAuthenticatedIpWhen)] + public Response AuthorizeIpAddress(RequestContext context, GameDatabaseContext database, ApiAuthenticateIpRequest body, GameUser user) + { + GameIp gameIp = database.GetIpFromAddress(user, body.IpAddress); + + if (!database.AuthorizeIpAddress(gameIp, body.OneTimeUse)) + return new Response(ConflictError.AlreadyAuthenticatedIpWhen, ContentType.Plaintext, HttpStatusCode.Conflict); + + return HttpStatusCode.Created; + } + [ApiEndpoint("gameAuth/ip/address/{address}", Method.Delete)] + [DocSummary("Deletes specified IP address.")] + public Response UnAuthorizeIpAddress(RequestContext context, GameDatabaseContext database, string address, GameUser user) + { + GameIp gameIp = database.GetIpFromAddress(user, address); + + database.RemoveIpAddress(gameIp); + return HttpStatusCode.NoContent; + } + + [ApiEndpoint("gameAuth/ip")] + [DocUsesPageData] + [DocSummary("List IP addresses that have attempted to connect with the user's username.")] + [DocQueryParam("authorized", "Filters authorized/unauthorized IP addresses from result.")] + public ApiListResponse GetAddresses(RequestContext context, GameDatabaseContext database, GameUser user) + { + (int from, int count, bool _) = PaginationHelper.GetPageData(context); + + bool? authorized = null; + if (bool.TryParse(context.QueryString["authorized"], out bool authorizedTemp)) authorized = authorizedTemp; + + (GameIp[] addresses, int totalAddresses) = + database.GetPaginatedIps(user, authorized, from, count); + + return new ApiListResponse(addresses.Select(a=>new ApiIpResponse(a)), totalAddresses); + } } \ No newline at end of file diff --git a/SoundShapesServer/Endpoints/Api/ApiIpAuthorizationEndpoints.cs b/SoundShapesServer/Endpoints/Api/ApiIpAuthorizationEndpoints.cs deleted file mode 100644 index ddaad923..00000000 --- a/SoundShapesServer/Endpoints/Api/ApiIpAuthorizationEndpoints.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Net; -using AttribDoc.Attributes; -using Bunkum.CustomHttpListener.Parsing; -using Bunkum.HttpServer; -using Bunkum.HttpServer.Endpoints; -using Bunkum.HttpServer.Responses; -using SoundShapesServer.Database; -using SoundShapesServer.Documentation.Attributes; -using SoundShapesServer.Documentation.Errors; -using SoundShapesServer.Helpers; -using SoundShapesServer.Requests.Api; -using SoundShapesServer.Responses.Api; -using SoundShapesServer.Types.Users; - -namespace SoundShapesServer.Endpoints.Api; - -public class ApiIpAuthorizationEndpoints : EndpointGroup -{ - [ApiEndpoint("ip/authorize", Method.Post)] - [DocSummary("Authorizes specified IP address.")] - [DocError(typeof(ConflictError), ConflictError.AlreadyAuthorizedIpWhen)] - public Response AuthorizeIpAddress(RequestContext context, GameDatabaseContext database, ApiAuthorizeIpRequest body, GameUser user) - { - Types.IpAuthorization ip = database.GetIpFromAddress(user, body.IpAddress); - - if (!database.AuthorizeIpAddress(ip, body.OneTimeUse)) - return new Response(ConflictError.AlreadyAuthorizedIpWhen, ContentType.Plaintext, HttpStatusCode.Conflict); - - return HttpStatusCode.Created; - } - [ApiEndpoint("ip/address/{address}", Method.Delete)] - [DocSummary("Deletes specified IP address.")] - public Response UnAuthorizeIpAddress(RequestContext context, GameDatabaseContext database, string address, GameUser user) - { - Types.IpAuthorization ip = database.GetIpFromAddress(user, address); - - database.RemoveIpAddress(ip); - return HttpStatusCode.NoContent; - } - - [ApiEndpoint("ip")] - [DocUsesPageData] - [DocSummary("Lists user's IP addresses.")] - [DocQueryParam("authorized", "Filters authorized/unauthorized IP addresses from result.")] - public ApiListResponse GetAddresses(RequestContext context, GameDatabaseContext database, GameUser user) - { - (int from, int count, bool _) = PaginationHelper.GetPageData(context); - - bool? authorized = null; - if (bool.TryParse(context.QueryString["authorized"], out bool authorizedTemp)) authorized = authorizedTemp; - - (Types.IpAuthorization[] addresses, int totalAddresses) = - database.GetPaginatedIps(user, authorized, from, count); - - return new ApiListResponse(addresses.Select(a=>new ApiIpResponse(a)), totalAddresses); - } -} \ No newline at end of file diff --git a/SoundShapesServer/Endpoints/Game/AuthenticationEndpoints.cs b/SoundShapesServer/Endpoints/Game/AuthenticationEndpoints.cs index bad34ea7..054dff5b 100644 --- a/SoundShapesServer/Endpoints/Game/AuthenticationEndpoints.cs +++ b/SoundShapesServer/Endpoints/Game/AuthenticationEndpoints.cs @@ -3,7 +3,10 @@ using Bunkum.HttpServer; using Bunkum.HttpServer.Endpoints; using Bunkum.HttpServer.Responses; +using Bunkum.HttpServer.Storage; using NPTicket; +using NPTicket.Verification; +using NPTicket.Verification.Keys; using Realms.Sync; using SoundShapesServer.Database; using SoundShapesServer.Responses.Game.Sessions; @@ -15,6 +18,7 @@ using SoundShapesServer.Types.Punishments; using SoundShapesServer.Types.Sessions; using SoundShapesServer.Types.Users; +using SoundShapesServer.Verification; using static SoundShapesServer.Helpers.IpHelper; using static SoundShapesServer.Helpers.PunishmentHelper; @@ -25,7 +29,7 @@ public class AuthenticationEndpoints : EndpointGroup [GameEndpoint("identity/login/token/psn.post", ContentType.Json, Method.Post)] [Endpoint("/identity/login/token/psn", ContentType.Json, Method.Post)] [Authentication(false)] - public Response? Login(RequestContext context, GameDatabaseContext database, Stream body, GameServerConfig config) + public Response? LogIn(RequestContext context, GameDatabaseContext database, Stream body, GameServerConfig config, IDataStore dataStore) { Ticket ticket; try @@ -37,7 +41,7 @@ public class AuthenticationEndpoints : EndpointGroup context.Logger.LogWarning(BunkumContext.Authentication, "Could not read ticket: " + e); return HttpStatusCode.BadRequest; } - + if (!UserHelper.IsUsernameLegal(ticket.Username)) return HttpStatusCode.BadRequest; GameUser? user = database.GetUserWithUsername(ticket.Username, true); @@ -46,26 +50,46 @@ public class AuthenticationEndpoints : EndpointGroup return new Response(HttpStatusCode.Created); } user ??= database.CreateUser(ticket.Username); - - IpAuthorization ip = GetIpAuthorizationFromRequestContext(context, database, user); - SessionType? sessionType = null; - + PlatformType platformType = PlatformHelper.GetPlatformType(ticket); + GameIp gameIp = GetGameIpFromRequestContext(context, database, user); + bool genuineTicket = VerifyTicket(context, (MemoryStream)body, ticket); + + SessionType? sessionType; + if (config.RequireAuthentication) { - if (user.HasFinishedRegistration == false || ip.Authorized == false) + if (!user.HasFinishedRegistration) + { + sessionType = SessionType.GameUnAuthorized; + } + else if (genuineTicket && ((platformType is PlatformType.Ps3 or PlatformType.PsVita && user.AllowPsnAuthentication) || + (platformType is PlatformType.Rpcs3 && user.AllowRpcnAuthentication))) + { + sessionType = SessionType.Game; + } + else if (gameIp.Authorized) + { + sessionType = SessionType.Game; + if (gameIp.OneTimeUse) + database.UseOneTimeIpAddress(gameIp); + } + else + { sessionType = SessionType.GameUnAuthorized; + } + } + else + { + sessionType = SessionType.Game; } - - if (GetActiveUserBans(user).Any()) sessionType = SessionType.Banned; - if (user.Deleted) sessionType = SessionType.GameUnAuthorized; - - PlatformType platformType = PlatformHelper.GetPlatformType(ticket); - sessionType ??= SessionType.Game; - GameSession session = database.CreateSession(user, (SessionType)sessionType, platformType, Globals.FourHoursInSeconds, null, ip); + if (GetActiveUserBans(user).Any()) + sessionType = SessionType.Banned; + if (user.Deleted) + sessionType = SessionType.GameUnAuthorized; - if (session.Ip is { OneTimeUse: true }) database.UseOneTimeIpAddress(session.Ip); + GameSession session = database.CreateSession(user, (SessionType)sessionType, platformType, genuineTicket, Globals.FourHoursInSeconds); GameSessionResponse sessionResponse = new (session); GameSessionWrapper responseWrapper = new (sessionResponse); @@ -73,15 +97,28 @@ public class AuthenticationEndpoints : EndpointGroup context.Logger.LogInfo(BunkumContext.Authentication, $"{sessionResponse.User.Username} has logged in."); context.ResponseHeaders.Add("set-cookie", $"OTG-Identity-SessionId={sessionResponse.Id};Version=1;Path=/"); - // ReSharper disable StringLiteralTypo - context.ResponseHeaders.Add("x-otg-identity-displayname", sessionResponse.User.Username); - context.ResponseHeaders.Add("x-otg-identity-personid", sessionResponse.User.Id); - context.ResponseHeaders.Add("x-otg-identity-sessionid", sessionResponse.Id); - // ReSharper restore StringLiteralTypo - return new Response(responseWrapper, ContentType.Json, HttpStatusCode.Created); } + + private static bool VerifyTicket(RequestContext context, MemoryStream body, Ticket ticket) + { + ITicketSigningKey signingKey; + // Determine the correct key to use + if (ticket.IssuerId == 0x33333333) + { + context.Logger.LogDebug(BunkumContext.Authentication, "Using RPCN ticket key"); + signingKey = RpcnSigningKey.Instance; + } + else + { + context.Logger.LogDebug(BunkumContext.Authentication, "Using PSN Sound Shapes ticket key"); + signingKey = SoundShapesSigningKey.Instance; + } + + TicketVerifier verifier = new(body.ToArray(), ticket, signingKey); + return verifier.IsTicketValid(); + } [GameEndpoint("~identity:*.hello"), Authentication(false)] public Response Hello(RequestContext context) @@ -94,47 +131,61 @@ public Response Hello(RequestContext context) { if (session?.SessionType == SessionType.Game) return EulaEndpoint.NormalEula(config); - - string? eula = null; + + string eulaEnd = $"\n \n{DateTime.UtcNow}"; if (user == null) { if (!config.AccountCreation) - eula = "Account Creation is disabled on this instance."; - else - return null; + return "Account Creation is disabled on this instance." + eulaEnd; + return null; } - - else if (user.Deleted) - eula = $"The account attached to your username ({user.Username}) has been deleted, and is no longer available."; - else + if (user.Deleted) + return $"The account attached to your username ({user.Username}) has been deleted, and is no longer available." + eulaEnd; + + IQueryable bans = GetActiveUserBans(user); + if (bans.Any()) { - IQueryable bans = GetActiveUserBans(user); - if (bans.Any()) - { - Punishment longestBan = bans.Last(); + Punishment longestBan = bans.Last(); - eula = "You are banned.\n" + - "Expires at " + longestBan.ExpiryDate.Date + ".\n" + - "Reason: \"" + longestBan.Reason + "\""; - } - else if (user.HasFinishedRegistration == false) - { - IpAuthorization ip = GetIpAuthorizationFromRequestContext(context, database, user); + return "You are banned.\n" + + "Expires at " + longestBan.ExpiryDate.Date + ".\n" + + "Reason: \"" + longestBan.Reason + "\"" + eulaEnd; + } + if (user.HasFinishedRegistration == false) + { + string emailSessionId = GenerateEmailSessionId(database); + database.CreateSession(user, SessionType.SetEmail, PlatformType.Api, null, Globals.TenMinutesInSeconds, emailSessionId); + return $"Your account is not registered.\n \n" + + $"To proceed, you will have to register an account at {config.WebsiteUrl}/register\n" + + $"Your email code is: {emailSessionId}" + eulaEnd; + } + + string unAuthorizedBase = $"Your session has not been authenticated.\n \n" + + $"To proceed, you will have to log into your account at {config.WebsiteUrl}/authentication " + + $"and perform one of the following actions:\n"; + + List authorizationMethods = new (); - string emailSessionId = GenerateEmailSessionId(database); - database.CreateSession(user, SessionType.SetEmail, session.PlatformType, Globals.TenMinutesInSeconds, emailSessionId, ip); - eula = $"Your account is not registered.\n" + - $"To proceed, you will have to register an account at {config.WebsiteUrl}/register\n" + - $"Your email code is: {emailSessionId}"; + if (session!.GenuineNpTicket == true) + { + switch (session.PlatformType) + { + case PlatformType.Ps3 or PlatformType.PsVita when !user.AllowPsnAuthentication: + authorizationMethods.Add("Enable PSN Authentication."); + break; + case PlatformType.Rpcs3 when !user.AllowRpcnAuthentication: + authorizationMethods.Add("Enable RPCN Authentication."); + break; } - else if (session.Ip is { Authorized: false }) - return $"Your IP address has not been authorized.\n" + - $"To proceed, you will have to log into your account at {config.WebsiteUrl}/authorization " + - $"and approve your IP address."; } - - return eula + $"\n-\n{DateTime.UtcNow}"; + + authorizationMethods.Add(user.AllowIpAuthentication + ? "Authorize your IP address." + : "Enable IP Authentication and authorize your IP address. (Note: You will have to reconnect upon enabling IP Authentication for your IP to show up.)"); + + string formattedMethods = authorizationMethods.Aggregate("", (current, method) => current + ("- " + method + "\n")); + return unAuthorizedBase + formattedMethods + eulaEnd; } } \ No newline at end of file diff --git a/SoundShapesServer/Endpoints/Game/EulaEndpoint.cs b/SoundShapesServer/Endpoints/Game/EulaEndpoint.cs index 6ae1f406..06fa9037 100644 --- a/SoundShapesServer/Endpoints/Game/EulaEndpoint.cs +++ b/SoundShapesServer/Endpoints/Game/EulaEndpoint.cs @@ -26,6 +26,6 @@ You should have received a copy of the GNU Affero General Public License // Gets called by AuthenticationEndpoints.cs public static string NormalEula(GameServerConfig config) { - return config.EulaText + "\n-\n" + AGPLLicense; + return config.EulaText + "\n \n" + AGPLLicense; } } \ No newline at end of file diff --git a/SoundShapesServer/GameServer.cs b/SoundShapesServer/GameServer.cs index d27c4c85..26e46d22 100644 --- a/SoundShapesServer/GameServer.cs +++ b/SoundShapesServer/GameServer.cs @@ -83,7 +83,7 @@ protected virtual void SetUpConfiguration() protected virtual void SetUpServices() { - ServerInstance.AddRateLimitService(Config!.RateLimitSettings); + ServerInstance.AddRateLimitService(new (30, 400, 0, "global")); ServerInstance.AddService(); ServerInstance.AddService(); ServerInstance.AddProfanityService(); diff --git a/SoundShapesServer/Helpers/IpHelper.cs b/SoundShapesServer/Helpers/IpHelper.cs index 5ee49dd2..371a1467 100644 --- a/SoundShapesServer/Helpers/IpHelper.cs +++ b/SoundShapesServer/Helpers/IpHelper.cs @@ -8,11 +8,11 @@ namespace SoundShapesServer.Helpers; public static class IpHelper { - public static IpAuthorization GetIpAuthorizationFromRequestContext(RequestContext context, GameDatabaseContext database, GameUser user) + public static GameIp GetGameIpFromRequestContext(RequestContext context, GameDatabaseContext database, GameUser user) { string ipAddress = ((IPEndPoint)context.RemoteEndpoint).Address.ToString(); - IpAuthorization ip = database.GetIpFromAddress(user, ipAddress); + GameIp gameIp = database.GetIpFromAddress(user, ipAddress); - return ip; + return gameIp; } } \ No newline at end of file diff --git a/SoundShapesServer/Helpers/PaginationHelper.cs b/SoundShapesServer/Helpers/PaginationHelper.cs index 71a5bab4..27838bff 100644 --- a/SoundShapesServer/Helpers/PaginationHelper.cs +++ b/SoundShapesServer/Helpers/PaginationHelper.cs @@ -58,7 +58,7 @@ public static GameAlbum[] PaginateAlbums(IQueryable albums, int from, return albums.AsEnumerable().Skip(from).Take(count).ToArray(); } - public static IpAuthorization[] PaginateIpAddresses(IQueryable addresses, int from, int count) + public static GameIp[] PaginateIpAddresses(IQueryable addresses, int from, int count) { return addresses.AsEnumerable().Skip(from).Take(count).ToArray(); } diff --git a/SoundShapesServer/Requests/Api/ApiAuthorizeIpRequest.cs b/SoundShapesServer/Requests/Api/ApiAuthenticateIpRequest.cs similarity index 85% rename from SoundShapesServer/Requests/Api/ApiAuthorizeIpRequest.cs rename to SoundShapesServer/Requests/Api/ApiAuthenticateIpRequest.cs index dcf2b65b..6fd505e2 100644 --- a/SoundShapesServer/Requests/Api/ApiAuthorizeIpRequest.cs +++ b/SoundShapesServer/Requests/Api/ApiAuthenticateIpRequest.cs @@ -2,7 +2,7 @@ namespace SoundShapesServer.Requests.Api; // ReSharper disable once ClassNeverInstantiated.Global -public class ApiAuthorizeIpRequest +public class ApiAuthenticateIpRequest { public string IpAddress { get; set; } public bool OneTimeUse { get; set; } diff --git a/SoundShapesServer/Requests/Api/ApiCreateAlbumRequest.cs b/SoundShapesServer/Requests/Api/ApiCreateAlbumRequest.cs index 21b6a0c5..634d5914 100644 --- a/SoundShapesServer/Requests/Api/ApiCreateAlbumRequest.cs +++ b/SoundShapesServer/Requests/Api/ApiCreateAlbumRequest.cs @@ -1,4 +1,3 @@ -// ReSharper disable UnusedAutoPropertyAccessor.Global #pragma warning disable CS8618 namespace SoundShapesServer.Requests.Api; diff --git a/SoundShapesServer/Requests/Api/ApiCreateCommunityTabRequest.cs b/SoundShapesServer/Requests/Api/ApiCreateCommunityTabRequest.cs index 5fed3983..f1a5536e 100644 --- a/SoundShapesServer/Requests/Api/ApiCreateCommunityTabRequest.cs +++ b/SoundShapesServer/Requests/Api/ApiCreateCommunityTabRequest.cs @@ -1,12 +1,9 @@ -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnassignedGetOnlyAutoProperty -// ReSharper disable UnusedAutoPropertyAccessor.Global - using SoundShapesServer.Types; #pragma warning disable CS8618 namespace SoundShapesServer.Requests.Api; +// ReSharper disable once ClassNeverInstantiated.Global public class ApiCreateCommunityTabRequest { public GameContentType ContentType { get; set; } diff --git a/SoundShapesServer/Requests/Api/ApiCreateDailyLevelRequest.cs b/SoundShapesServer/Requests/Api/ApiCreateDailyLevelRequest.cs index bcbe5eab..663395a9 100644 --- a/SoundShapesServer/Requests/Api/ApiCreateDailyLevelRequest.cs +++ b/SoundShapesServer/Requests/Api/ApiCreateDailyLevelRequest.cs @@ -1,4 +1,3 @@ -// ReSharper disable UnusedAutoPropertyAccessor.Global #pragma warning disable CS8618 namespace SoundShapesServer.Requests.Api; diff --git a/SoundShapesServer/Requests/Api/ApiCreateNewsEntryRequest.cs b/SoundShapesServer/Requests/Api/ApiCreateNewsEntryRequest.cs index d521cbb2..56008092 100644 --- a/SoundShapesServer/Requests/Api/ApiCreateNewsEntryRequest.cs +++ b/SoundShapesServer/Requests/Api/ApiCreateNewsEntryRequest.cs @@ -1,4 +1,3 @@ -// ReSharper disable UnusedAutoPropertyAccessor.Global namespace SoundShapesServer.Requests.Api; // ReSharper disable once ClassNeverInstantiated.Global diff --git a/SoundShapesServer/Requests/Api/ApiPunishRequest.cs b/SoundShapesServer/Requests/Api/ApiPunishRequest.cs index 4416b64b..b6e934d9 100644 --- a/SoundShapesServer/Requests/Api/ApiPunishRequest.cs +++ b/SoundShapesServer/Requests/Api/ApiPunishRequest.cs @@ -1,5 +1,4 @@ // ReSharper disable UnassignedGetOnlyAutoProperty -// ReSharper disable UnusedAutoPropertyAccessor.Global using SoundShapesServer.Types.Punishments; diff --git a/SoundShapesServer/Requests/Api/ApiReportRequest.cs b/SoundShapesServer/Requests/Api/ApiReportRequest.cs index ed478dab..95658c19 100644 --- a/SoundShapesServer/Requests/Api/ApiReportRequest.cs +++ b/SoundShapesServer/Requests/Api/ApiReportRequest.cs @@ -1,5 +1,4 @@ using SoundShapesServer.Types.Reports; -// ReSharper disable UnusedAutoPropertyAccessor.Global #pragma warning disable CS8618 namespace SoundShapesServer.Requests.Api; diff --git a/SoundShapesServer/Requests/Api/ApiSetGameAuthenticationSettingsRequest.cs b/SoundShapesServer/Requests/Api/ApiSetGameAuthenticationSettingsRequest.cs new file mode 100644 index 00000000..8360a0e5 --- /dev/null +++ b/SoundShapesServer/Requests/Api/ApiSetGameAuthenticationSettingsRequest.cs @@ -0,0 +1,10 @@ +namespace SoundShapesServer.Requests.Api; +#pragma warning disable CS8618 + +// ReSharper disable once ClassNeverInstantiated.Global +public class ApiSetGameAuthenticationSettingsRequest +{ + public bool AllowPsnAuthentication { get; set; } + public bool AllowRpcnAuthentication { get; set; } + public bool AllowIpAuthentication { get; set; } +} \ No newline at end of file diff --git a/SoundShapesServer/Responses/Api/ApiGameAuthenticationSettingsResponse.cs b/SoundShapesServer/Responses/Api/ApiGameAuthenticationSettingsResponse.cs new file mode 100644 index 00000000..0294e82b --- /dev/null +++ b/SoundShapesServer/Responses/Api/ApiGameAuthenticationSettingsResponse.cs @@ -0,0 +1,17 @@ +using SoundShapesServer.Types.Users; + +namespace SoundShapesServer.Responses.Api; + +public class ApiGameAuthenticationSettingsResponse +{ + public ApiGameAuthenticationSettingsResponse(GameUser user) + { + AllowPsnAuthentication = user.AllowPsnAuthentication; + AllowRpcnAuthentication = user.AllowRpcnAuthentication; + AllowIpAuthentication = user.AllowIpAuthentication; + } + + public bool AllowPsnAuthentication { get; set; } + public bool AllowRpcnAuthentication { get; set; } + public bool AllowIpAuthentication { get; set; } +} \ No newline at end of file diff --git a/SoundShapesServer/Responses/Api/ApiIpResponse.cs b/SoundShapesServer/Responses/Api/ApiIpResponse.cs index 30397423..ea865811 100644 --- a/SoundShapesServer/Responses/Api/ApiIpResponse.cs +++ b/SoundShapesServer/Responses/Api/ApiIpResponse.cs @@ -3,13 +3,13 @@ namespace SoundShapesServer.Responses.Api; // ReSharper disable once ClassNeverInstantiated.Global public class ApiIpResponse : IApiResponse { - public ApiIpResponse(Types.IpAuthorization ip) + public ApiIpResponse(Types.GameIp gameIp) { - IpAddress = ip.IpAddress; - Authorized = ip.Authorized; - OneTimeUse = ip.OneTimeUse; - CreationDate = ip.CreationDate.ToUnixTimeSeconds(); - ModificationDate = ip.ModificationDate.ToUnixTimeSeconds(); + IpAddress = gameIp.IpAddress; + Authorized = gameIp.Authorized; + OneTimeUse = gameIp.OneTimeUse; + CreationDate = gameIp.CreationDate.ToUnixTimeSeconds(); + ModificationDate = gameIp.ModificationDate.ToUnixTimeSeconds(); } public string IpAddress { get; set; } diff --git a/SoundShapesServer/SoundShapesServer.csproj b/SoundShapesServer/SoundShapesServer.csproj index ac39f34f..3ae22069 100644 --- a/SoundShapesServer/SoundShapesServer.csproj +++ b/SoundShapesServer/SoundShapesServer.csproj @@ -21,4 +21,5 @@ + diff --git a/SoundShapesServer/Types/IpAuthorization.cs b/SoundShapesServer/Types/GameIp.cs similarity index 75% rename from SoundShapesServer/Types/IpAuthorization.cs rename to SoundShapesServer/Types/GameIp.cs index f19719a9..da0e6b7f 100644 --- a/SoundShapesServer/Types/IpAuthorization.cs +++ b/SoundShapesServer/Types/GameIp.cs @@ -6,9 +6,9 @@ namespace SoundShapesServer.Types; -public class IpAuthorization : RealmObject +public class GameIp : RealmObject { - public IpAuthorization(string ipAddress, GameUser user) + public GameIp(string ipAddress, GameUser user) { IpAddress = ipAddress; User = user; @@ -17,13 +17,13 @@ public IpAuthorization(string ipAddress, GameUser user) } // Realm cries if this doesn't exist - public IpAuthorization() {} + public GameIp() {} public string IpAddress { get; init; } public bool Authorized { get; set; } public bool OneTimeUse { get; set; } public GameUser User { get; init; } - [Backlink(nameof(GameSession.Ip))] public IQueryable Sessions { get; } + public IList Sessions { get; } public DateTimeOffset CreationDate { get; set; } public DateTimeOffset ModificationDate { get; set; } } \ No newline at end of file diff --git a/SoundShapesServer/Types/Sessions/GameSession.cs b/SoundShapesServer/Types/Sessions/GameSession.cs index 6ffc952b..3faabbce 100644 --- a/SoundShapesServer/Types/Sessions/GameSession.cs +++ b/SoundShapesServer/Types/Sessions/GameSession.cs @@ -28,7 +28,7 @@ public PlatformType PlatformType get => (PlatformType)_PlatformType; set => _PlatformType = (int)value; } - public IpAuthorization? Ip { get; init; } public DateTimeOffset CreationDate { get; init; } public DateTimeOffset ExpiryDate { get; init; } + public bool? GenuineNpTicket { get; init; } } \ No newline at end of file diff --git a/SoundShapesServer/Types/Users/GameUser.cs b/SoundShapesServer/Types/Users/GameUser.cs index ed82a9d5..b35fae3b 100644 --- a/SoundShapesServer/Types/Users/GameUser.cs +++ b/SoundShapesServer/Types/Users/GameUser.cs @@ -28,14 +28,17 @@ public PermissionsType PermissionsType public string? Email { get; set; } public string? PasswordBcrypt { get; set; } public bool HasFinishedRegistration { get; set; } + public bool AllowPsnAuthentication { get; set; } + public bool AllowRpcnAuthentication { get; set; } + public bool AllowIpAuthentication { get; set; } public bool Deleted { get; init; } public DateTimeOffset CreationDate { get; init; } public DateTimeOffset LastGameLogin { get; set; } public string? SaveFilePath { get; set; } // ReSharper disable all UnassignedGetOnlyAutoProperty - [Backlink(nameof(IpAuthorization.User))] - public IQueryable IpAddresses { get; } + [Backlink(nameof(GameIp.User))] + public IQueryable IpAddresses { get; } [Backlink(nameof(LevelLikeRelation.User))] public IQueryable LikedLevels { get; } public int LikedLevelsCount { get; set; } diff --git a/SoundShapesServer/Verification/SoundShapesSigningKey.cs b/SoundShapesServer/Verification/SoundShapesSigningKey.cs new file mode 100644 index 00000000..dba72fb2 --- /dev/null +++ b/SoundShapesServer/Verification/SoundShapesSigningKey.cs @@ -0,0 +1,12 @@ +using NPTicket.Verification.Keys; + +namespace SoundShapesServer.Verification; + +public class SoundShapesSigningKey : PsnSigningKey +{ + public static readonly SoundShapesSigningKey Instance = new(); + private SoundShapesSigningKey() {} + + public override string CurveX => "39c62d061d4ee35c5f3f7531de0af3cf918346526edac727"; + public override string CurveY => "a5d578b55113e612bf1878d4cc939d61a41318403b5bdf86"; +} \ No newline at end of file diff --git a/SoundShapesServerTests/TestContext.cs b/SoundShapesServerTests/TestContext.cs index e3d2368d..fa12ff51 100644 --- a/SoundShapesServerTests/TestContext.cs +++ b/SoundShapesServerTests/TestContext.cs @@ -35,27 +35,31 @@ public HttpClient GetAuthenticatedClient(SessionType type, return GetAuthenticatedClient(type, out _, user, tokenExpirySeconds); } - public HttpClient GetAuthenticatedClient(SessionType type, out string sessionId, + public HttpClient GetAuthenticatedClient(SessionType sessionType, out string sessionId, GameUser? user = null, int tokenExpirySeconds = GameDatabaseContext.DefaultSessionExpirySeconds) { user ??= CreateUser(); // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault - PlatformType platformType = type switch + PlatformType platformType = sessionType switch { SessionType.Game => PlatformType.PsVita, SessionType.Api => PlatformType.Api, - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + _ => throw new ArgumentOutOfRangeException(nameof(sessionType), sessionType, null) }; - GameSession session = Database.CreateSession(user, type, platformType, tokenExpirySeconds); + bool? genuineTicket = null; + if (sessionType == SessionType.Game) + genuineTicket = true; + + GameSession session = Database.CreateSession(user, sessionType, platformType, genuineTicket, tokenExpirySeconds); sessionId = session.Id; HttpClient client = Listener.GetClient(); // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression - if (type is SessionType.Game or SessionType.Banned or SessionType.GameUnAuthorized) + if (sessionType is SessionType.Game or SessionType.Banned or SessionType.GameUnAuthorized) { client.DefaultRequestHeaders.TryAddWithoutValidation("X-OTG-Identity-SessionId", session.Id); }